Add project files
build / linux-clang (push) Canceled after 0s
build / linux-gcc (push) Canceled after 0s
build / macos-clang (push) Canceled after 0s
build / windows-mingw (push) Canceled after 0s
build / windows-msvc (push) Canceled after 0s

This commit is contained in:
alejo committed 2026-10-07 00:25:24 -03:00
commit ba5971a788
37 files changed
+11180

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
name: build
on:
push:
pull_request:
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { name: linux-gcc, os: ubuntu-latest, cc: gcc, cxx: g++ }
- { name: linux-clang, os: ubuntu-latest, cc: clang, cxx: clang++ }
- { name: windows-msvc, os: windows-latest }
- { name: windows-mingw, os: windows-latest, generator: MinGW Makefiles, cc: gcc, cxx: g++ }
- { name: macos-clang, os: macos-latest }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Configure
shell: bash
run: |
args=(-B build -DCMAKE_BUILD_TYPE=Release -DANTLRLAB_DOWNLOAD_JAR=ON)
if [ -n "${{ matrix.generator }}" ]; then args+=(-G "${{ matrix.generator }}"); fi
if [ -n "${{ matrix.cc }}" ]; then args+=(-DCMAKE_C_COMPILER=${{ matrix.cc }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx }}); fi
cmake "${args[@]}"
- name: Build
run: cmake --build build --config Release --parallel
- name: Self-test and examples
shell: bash
run: |
BIN=$(find build -type f \( -name antlrlab -o -name antlrlab.exe \) -not -path '*/CMakeFiles/*' | head -n 1)
echo "using $BIN"
"$BIN" --version
"$BIN" --selftest
"$BIN" examples/combined/Expr.g4 -t "1 + 2" --no-color
"$BIN" -g examples/split/CalcLexer.g4 -g examples/split/CalcParser.g4 -i examples/split/input.txt -q --no-color
"$BIN" examples/json/JSON.g4 examples/json/example.json -q --no-color
# a syntax error must give exit status 1
set +e
"$BIN" examples/combined/Expr.g4 examples/combined/input_bad.txt -q --no-color
test $? -eq 1
+3
View File
@@ -0,0 +1,3 @@
antlr-lab-out/
build/
*.jar
+180
View File
@@ -0,0 +1,180 @@
cmake_minimum_required(VERSION 3.16)
# std::filesystem needs macOS 10.15 or newer. Has to be set before project().
if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum macOS version")
endif()
project(antlrlab VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
# The ANTLR tool (Java) and the C++ runtime must be the same version, because
# the serialized grammar that the tool writes is read by the runtime.
set(ANTLR_VERSION "4.13.2" CACHE STRING "ANTLR version to build against")
option(ANTLRLAB_USE_SYSTEM_RUNTIME
"Use an installed ANTLR C++ runtime instead of downloading one" OFF)
option(ANTLRLAB_DOWNLOAD_JAR
"Download antlr-${ANTLR_VERSION}-complete.jar at configure time and use it by default" OFF)
if(MSVC)
option(ANTLRLAB_STATIC_CRT
"Visual C++: link the C++ runtime statically, so the program needs no redistributable" ON)
elseif(MINGW)
option(ANTLRLAB_STATIC_LINK
"MinGW: link libgcc, libstdc++ and winpthread statically, so no DLLs have to ship with the program" ON)
endif()
find_package(Threads REQUIRED)
# Visual C++: the runtime and antlrlab have to use the same C runtime library.
if(MSVC)
if(ANTLRLAB_STATIC_CRT)
set(WITH_STATIC_CRT ON CACHE BOOL "" FORCE) # read by the ANTLR runtime's own CMakeLists
set(ANTLRLAB_MSVC_RUNTIME "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
set(WITH_STATIC_CRT OFF CACHE BOOL "" FORCE)
set(ANTLRLAB_MSVC_RUNTIME "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
endif()
# --------------------------------------------------------------- runtime ----
if(ANTLRLAB_USE_SYSTEM_RUNTIME)
find_path(ANTLR4_INCLUDE_DIR antlr4-runtime.h
PATH_SUFFIXES antlr4-runtime include/antlr4-runtime)
find_library(ANTLR4_LIBRARY NAMES antlr4-runtime antlr4-runtime-static)
if(NOT ANTLR4_INCLUDE_DIR OR NOT ANTLR4_LIBRARY)
message(FATAL_ERROR
"ANTLR C++ runtime not found. Install it, set ANTLR4_INCLUDE_DIR and "
"ANTLR4_LIBRARY, or configure without -DANTLRLAB_USE_SYSTEM_RUNTIME=ON.")
endif()
set(ANTLRLAB_RUNTIME_LIBS ${ANTLR4_LIBRARY})
set(ANTLRLAB_RUNTIME_INCLUDES ${ANTLR4_INCLUDE_DIR})
set(ANTLRLAB_RUNTIME_DEFS "")
else()
include(FetchContent)
set(ANTLR_BUILD_CPP_TESTS OFF CACHE BOOL "" FORCE)
set(ANTLR_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ANTLR_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(WITH_DEMO OFF CACHE BOOL "" FORCE)
set(ANTLR4_INSTALL OFF CACHE BOOL "" FORCE)
set(_antlr_fetch_args
URL "https://github.com/antlr/antlr4/archive/refs/tags/${ANTLR_VERSION}.tar.gz"
SOURCE_SUBDIR runtime/Cpp)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
list(APPEND _antlr_fetch_args DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
endif()
FetchContent_Declare(antlr4 ${_antlr_fetch_args})
FetchContent_MakeAvailable(antlr4)
set(ANTLRLAB_RUNTIME_LIBS antlr4_static)
set(ANTLRLAB_RUNTIME_INCLUDES "${antlr4_SOURCE_DIR}/runtime/Cpp/runtime/src")
set(ANTLRLAB_RUNTIME_DEFS ANTLR4CPP_STATIC)
endif()
# ------------------------------------------------------------------- jar ----
set(ANTLRLAB_JAR_PATH "")
if(ANTLRLAB_DOWNLOAD_JAR)
set(_jar "${CMAKE_BINARY_DIR}/antlr-${ANTLR_VERSION}-complete.jar")
if(NOT EXISTS "${_jar}")
# antlr.org first, then the copy in the ANTLR website's GitHub repository.
foreach(_url
"https://www.antlr.org/download/antlr-${ANTLR_VERSION}-complete.jar"
"https://raw.githubusercontent.com/antlr/website-antlr4/gh-pages/download/antlr-${ANTLR_VERSION}-complete.jar")
message(STATUS "Downloading ${_url}")
file(DOWNLOAD "${_url}" "${_jar}" STATUS _dl_status SHOW_PROGRESS)
list(GET _dl_status 0 _dl_code)
if(_dl_code EQUAL 0)
break()
endif()
file(REMOVE "${_jar}")
message(STATUS " failed: ${_dl_status}")
endforeach()
if(NOT EXISTS "${_jar}")
message(WARNING "Could not download the ANTLR jar. "
"Pass --antlr-jar or set ANTLR_JAR when running antlrlab.")
endif()
endif()
if(EXISTS "${_jar}")
file(TO_CMAKE_PATH "${_jar}" ANTLRLAB_JAR_PATH)
endif()
endif()
# ---------------------------------------------------------------- target ----
add_executable(antlrlab src/main.cpp)
target_include_directories(antlrlab PRIVATE src ${ANTLRLAB_RUNTIME_INCLUDES})
target_link_libraries(antlrlab PRIVATE ${ANTLRLAB_RUNTIME_LIBS} Threads::Threads)
target_compile_definitions(antlrlab PRIVATE
${ANTLRLAB_RUNTIME_DEFS}
LAB_VERSION="${PROJECT_VERSION}"
LAB_ANTLR_VERSION="${ANTLR_VERSION}")
if(ANTLRLAB_JAR_PATH)
target_compile_definitions(antlrlab PRIVATE LAB_DEFAULT_ANTLR_JAR="${ANTLRLAB_JAR_PATH}")
# Also put the jar next to the program: antlrlab looks there too, which keeps
# working when the build directory is moved, or its path has unusual characters.
add_custom_command(TARGET antlrlab POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ANTLRLAB_JAR_PATH}" "$<TARGET_FILE_DIR:antlrlab>")
install(FILES "${ANTLRLAB_JAR_PATH}" DESTINATION bin)
endif()
# std::filesystem lives in a separate library in GCC 8 and older, and in Clang
# with libc++ 8 and older.
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
if(MSVC)
set(CMAKE_REQUIRED_FLAGS "/std:c++17")
endif()
check_cxx_source_compiles("#include <filesystem>
int main() { return std::filesystem::current_path().empty() ? 1 : 0; }" ANTLRLAB_FS_BUILTIN)
unset(CMAKE_REQUIRED_FLAGS)
if(NOT ANTLRLAB_FS_BUILTIN)
foreach(_fslib stdc++fs c++fs)
set(CMAKE_REQUIRED_LIBRARIES ${_fslib})
check_cxx_source_compiles("#include <filesystem>
int main() { return std::filesystem::current_path().empty() ? 1 : 0; }" ANTLRLAB_FS_${_fslib})
unset(CMAKE_REQUIRED_LIBRARIES)
if(ANTLRLAB_FS_${_fslib})
target_link_libraries(antlrlab PRIVATE ${_fslib})
break()
endif()
endforeach()
endif()
if(WIN32)
# Winsock (web UI), BCrypt (access token), Shell32 (command line, opening the
# browser), DbgHelp (stack traces in crash reports)
target_link_libraries(antlrlab PRIVATE ws2_32 bcrypt shell32 dbghelp)
target_compile_definitions(antlrlab PRIVATE _WIN32_WINNT=0x0601 NOMINMAX WIN32_LEAN_AND_MEAN)
endif()
# Crash reports print a stack trace; ENABLE_EXPORTS (-rdynamic) makes it show function names.
if(NOT WIN32)
set_target_properties(antlrlab PROPERTIES ENABLE_EXPORTS ON)
endif()
# The ANTLR runtime recurses deeply on real grammars. antlrlab runs it on a
# thread with a big stack it creates itself (src/isolate.hpp), so the main
# program only needs a normal stack; 8 MB matches the Linux default for the
# threads of the web server.
if(MSVC)
target_link_options(antlrlab PRIVATE /STACK:8388608)
set_property(TARGET antlrlab PROPERTY MSVC_RUNTIME_LIBRARY "${ANTLRLAB_MSVC_RUNTIME}")
target_compile_options(antlrlab PRIVATE /utf-8 /EHsc /permissive- /W3)
target_compile_definitions(antlrlab PRIVATE _CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS)
else()
target_compile_options(antlrlab PRIVATE -Wall -Wextra)
if(MINGW)
target_link_options(antlrlab PRIVATE -Wl,--stack,8388608)
if(ANTLRLAB_STATIC_LINK)
target_link_options(antlrlab PRIVATE -static -static-libgcc -static-libstdc++)
endif()
endif()
endif()
install(TARGETS antlrlab RUNTIME DESTINATION bin)
+362
View File
@@ -0,0 +1,362 @@
# antlrlab
A command line version of [antlr-lab](http://lab.antlr.org/) written in C++. Give it
your ANTLR grammar files and an input file, and it produces the parse tree
diagram, the tree as text, the token list, and a syntax check of the input.
```
$ antlrlab examples/combined/Expr.g4 -t "1 + 2"
(prog (expr (expr 1) + (expr 2)) <EOF>)
Input is valid (3 tokens, 8 nodes)
Report: antlr-lab-out/report.html
```
Open `antlr-lab-out/report.html` for the diagram (pan, zoom, save as SVG or PNG),
the tree, the tokens, the problems and the grammar. Hovering a node or a token
highlights the text it came from in the input.
Prefer an editor? `antlrlab --serve Expr.g4 input.txt --open` starts a
[web UI](#web-ui) where you edit the grammar and the inputs, save them to disk,
and watch the diagram update.
## Build
You need a C++17 compiler, CMake 3.16 or newer, Java 11 or newer (the ANTLR tool
is a Java program), and network access for the first configure.
The code is standard C++17 with the operating-system parts isolated in
`src/platform.hpp` and `src/crash.hpp`, so it builds with **GCC**, **Clang**,
**MSVC** (also `clang-cl`) and **MinGW-w64** (the *posix* thread model; the
*win32* one has no `std::thread`), on **Linux, Windows and macOS**. Compilers
older than GCC 9 or Clang 9 get `-lstdc++fs` / `-lc++fs` automatically. What has
actually been run is listed under [Limitations](#limitations).
```sh
cmake -B build -DANTLRLAB_DOWNLOAD_JAR=ON
cmake --build build
./build/antlrlab --version
```
On Windows with Visual Studio, in a *Developer Command Prompt* or PowerShell
(any generator works; Visual Studio's is multi-configuration, so name one):
```bat
cmake -B build -DANTLRLAB_DOWNLOAD_JAR=ON
cmake --build build --config Release
build\Release\antlrlab.exe --selftest
```
With MinGW (MSYS2 `mingw-w64-x86_64-gcc` and `cmake`) use
`-G "MinGW Makefiles"` or `-G Ninja`. To cross-compile for Windows from Linux:
```sh
sudo apt install g++-mingw-w64-x86-64-posix
cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake -DANTLRLAB_DOWNLOAD_JAR=ON
cmake --build build-win
```
The configure step downloads the ANTLR C++ runtime (source, built statically) and,
with `ANTLRLAB_DOWNLOAD_JAR=ON`, `antlr-4.13.2-complete.jar` (from antlr.org, or
from the ANTLR website's GitHub repository if that is unreachable). The jar is
also copied next to the program, where antlrlab looks for it. Without that
option, tell antlrlab where the jar is when you run it (see below).
| CMake option | Default | Meaning |
| --- | --- | --- |
| `ANTLR_VERSION` | `4.13.2` | Version of the runtime (and of the jar to download) |
| `ANTLRLAB_DOWNLOAD_JAR` | `OFF` | Download the tool jar and use it as the default |
| `ANTLRLAB_USE_SYSTEM_RUNTIME` | `OFF` | Link an installed runtime instead of downloading one |
| `ANTLRLAB_STATIC_CRT` | `ON` | MSVC: link the C++ runtime statically (no redistributable needed) |
| `ANTLRLAB_STATIC_LINK` | `ON` | MinGW: link libgcc, libstdc++ and winpthread statically (no DLLs to ship) |
`.github/workflows/build.yml` builds and self-tests all of this (Linux with GCC
and Clang, Windows with MSVC and MinGW, macOS) on every push.
The tool and the runtime must be the same version. If you use your own jar or
runtime, use the same release for both (4.10 or newer).
## Use
```sh
# one combined grammar
antlrlab Expr.g4 input.txt
# separate lexer and parser grammars
antlrlab -g CalcLexer.g4 -g CalcParser.g4 -i input.txt
# re-run on every save, and keep report.html refreshed in your browser
antlrlab Expr.g4 input.txt --watch --open
# other rules, other input sources
antlrlab Expr.g4 -s expr -t "1 + 2"
echo "1 + 2" | antlrlab Expr.g4 -i -
```
| Option | Meaning |
| --- | --- |
| `-g, --grammar FILE` | Grammar file. Repeat for a lexer plus a parser. Files ending in `.g4` given as plain arguments count too. |
| `-i, --input FILE` | Input file, or `-` for standard input. Any non-`.g4` plain argument is also taken as the input. |
| `-t, --text TEXT` | Input given on the command line. |
| `-s, --start RULE` | Start rule. Default: the first parser rule. |
| `-o, --out DIR` | Output directory. Default: `antlr-lab-out`. |
| `-l, --lib DIR` | Directory with imported grammars or `.tokens` files. |
| `--antlr-jar FILE` | The ANTLR tool jar. |
| `-w, --watch` | Re-run when a grammar or the input file changes. |
| `--open` | Open `report.html` in the default browser. |
| `--tokens` | Also print the token list. |
| `--serve` | Start the [web UI](#web-ui) instead of running once. Plain arguments become open tabs. |
| `-p, --port N` | Port for `--serve` (default: the first free port from 8765). |
| `--root DIR` | Directory the web UI may read and write (default: the current directory). |
| `-q, --quiet` | Print only diagnostics and the verdict. |
| `--no-color` | No colors (`NO_COLOR` is honoured too). |
| `--timeout SECS` | Stop a run that takes longer (default 120; `0` = no limit). Not enforced on Windows. |
| `--no-isolate` | Run the ANTLR runtime inside the main process instead of a child process. For debuggers; see [If it crashes](#if-it-crashes). (Windows always does this.) |
| `--selftest` | Check the installation with a built-in grammar, then exit. |
Exit status: `0` the input is valid, `1` it has syntax errors, `2` a grammar, tool
or usage error. That makes it usable in scripts and CI.
### Finding the ANTLR tool
In this order: `--antlr-jar`, the `ANTLR_JAR` environment variable, the jar
downloaded at build time, an `antlr-*-complete.jar` found in the current
directory, next to the program, on `CLASSPATH`, in your home directory or in
`/usr/local/lib`, `/usr/share/java` and the Homebrew directories (on Windows:
`C:\Javalib`, `C:\antlr`, `C:\Program Files\ANTLR`), and finally an `antlr4` or
`antlr` command on `PATH` (a `.exe`, `.cmd` or `.bat` on Windows). Java comes
from `JAVA_HOME` or `PATH`.
File names and paths may contain any Unicode characters. On Windows they are
converted properly (the Windows ANSI code page is never used), and the Java tool,
which only understands that code page, is given the short 8.3 form of a path
when it contains characters outside it. On Linux, Java needs a UTF-8 locale
(`LANG=C.UTF-8`) to open files with non-ASCII names.
### Output files
| File | Content |
| --- | --- |
| `report.html` | The report: a single self-contained page, no network needed |
| `tree.svg` | The diagram as a standalone SVG |
| `tree.txt` | The tree in ANTLR's LISP form |
| `tokens.txt` | Tokens as `[@index,start:stop='text',<TYPE>,line:col]` |
| `problems.txt` | Syntax errors and warnings |
| `generated/` | What the ANTLR tool wrote (the `.interp` files are what matters) |
## Web UI
```sh
antlrlab --serve # browse the current directory
antlrlab --serve Expr.g4 input.txt # start with these files open
antlrlab --serve --root ~/grammars --open
```
antlrlab prints an address such as `http://127.0.0.1:8765/?token=...`; `--open`
opens it for you. It is a local web app that edits **real files on disk**:
```
File Edit View
+------------------------------+ +--------------------------------+
| Expr.g4 G | ok.txt I | in2 I | | Diagram (ok.txt) [valid] x |
| Start rule [ ] Auto-run Run| | Diagram | Tree | Tokens |
| | | |
| one editor, one tab per | | the parse tree |
| grammar or input file | +--------------------------------+
| | | Output (Expr) x |
| | | Problems | ANTLR tool |
+------------------------------+ +--------------------------------+
```
- **One editor, many files.** Each grammar and input is a tab. The `G` / `I`
badge says how the file is used; click it to switch (for example to parse a
`.g4` file as input). The diagram follows the active input, the Output panel
follows the active grammar.
- **Run uses what is in the editor**, saved or not, so you can try an edit
before touching the file. With *Auto-run* on (View menu) it re-runs shortly
after you stop typing. The Java tool only runs again when a grammar actually
changed. `Ctrl+Enter` runs immediately.
- **Lexer + parser pairs work.** If the active grammar is a parser with
`tokenVocab=X`, `X.g4` is taken from an open tab or from the same folder; if
it is a lexer, the parser that uses it is found the same way.
- **Real files.** `Ctrl+S` saves in place (line endings and BOM are kept),
*Save as* picks a folder and name inside the workspace, *Open* browses it.
Every two seconds the open files are checked against the disk: a clean tab
reloads silently when another program changes the file; a tab with unsaved
edits shows a banner instead, and saving over a changed file asks first.
- **Errors where they are.** Syntax errors in the input are underlined and
listed under *Problems*; ANTLR tool errors are marked in the grammar at the
reported line. Click a problem to jump to it. Hovering or clicking a node in
the diagram, or a row in *Tokens*, highlights its text in the input.
- **Diagram** with zoom (wheel), pan (drag), *Fit*, and SVG / PNG export. *Tree*
shows the LISP form (indented or on one line, with Copy), *Tokens* the token
table.
- **Editor niceties:** grammar syntax colors, line numbers, auto-indent,
`Tab` / `Shift+Tab` indent, `Ctrl+/` comment, `Ctrl+F` find and replace.
- Panels close with their `x` and come back from the View menu; the splitters
drag. Layout, theme (system / light / dark) and text size are remembered, and
so are the open files per workspace.
| Shortcut | Action |
| --- | --- |
| `Ctrl+Enter` | Run |
| `Ctrl+S`, `Ctrl+Shift+S` | Save, Save as |
| `Ctrl+O` | Open file |
| `Alt+W` | Close tab |
| `Ctrl+F` | Find and replace |
| `Ctrl+/` | Toggle comment (grammar) |
### Is it safe to leave running?
It is built for your own machine, and a page that can write files deserves care:
- It listens on `127.0.0.1` only, never on the network.
- Every request needs a random token that is generated at start-up and sent in
the printed address (it moves into a cookie and out of the address bar). The
`Host` and `Origin` headers are checked, so other web pages and DNS-rebinding
tricks cannot talk to it.
- Reads and writes are confined to the workspace directory: paths are resolved
(symlinks too) and anything outside is refused. `.git` is off limits, and
hidden files are not listed.
- It refuses to serve the root of the file system.
Opening a file larger than 5 MB or an input larger than 4 MB is refused.
### Changing the UI
The browser code lives in `web/` (plain HTML, CSS and JavaScript, no build
tools or libraries). After editing it, run `python3 tools/embed_web.py` to
regenerate `src/webui_assets.hpp`, which is checked in, so building antlrlab
itself never needs Python.
## How it works
The ANTLR C++ runtime can run a grammar but cannot read a `.g4` file, and compiling
generated C++ for every change would be slow. So:
1. antlrlab runs the ANTLR tool (Java) on your grammars. The tool writes
`.interp` files next to the generated code: the serialized grammar plus the
token and rule names.
2. The `.interp` data is loaded into the C++ runtime's `LexerInterpreter` and
`ParserInterpreter`, which run the grammar directly. No generated code is ever
compiled.
3. The resulting parse tree is copied into a plain tree that the diagram, text and
HTML code work from.
The runtime part (steps 2 and 3) runs in an isolated worker, described under
[If it crashes](#if-it-crashes).
Like the online lab, actions and semantic predicates are ignored, and the parser
does not insist on reaching the end of the input: if the start rule stops early
you get a warning (end the rule with `EOF` to require a full match).
## If it crashes
The ANTLR C++ runtime is a native library, and a bad grammar, a very deep input
or a bug can crash it. antlrlab is built so that this does not take the program
down:
- The runtime runs in a **child process**, on a thread with a **512 MB stack**
(falling back to smaller sizes if the system refuses). Deep recursion, which
is the usual cause of crashes in the runtime on real-world grammars, simply
works; and if something still crashes, only the child dies. The web UI keeps
running and shows *Run crashed*; the command line prints the error, writes the
report and exits with status 2 (in `--watch` mode it keeps watching).
- A run longer than `--timeout` seconds (default 120) is stopped.
- The loaded grammar data is checked against the rule names before the
interpreters use it, so an inconsistent `.interp` file is reported as an error
rather than causing an invalid memory access.
When a crash happens, the terminal shows what it was doing and a stack trace:
```
[antlrlab] fatal error: SIGSEGV (invalid memory access, often a stack overflow)
[antlrlab] while: parsing the input (parser)
[antlrlab] address: 0x0
[antlrlab] stack trace (innermost first):
./antlrlab(_ZN6antlr4...+0x6f1)[0x558f71ae3e17]
...
```
Pipe the trace through `c++filt` to demangle the names.
One hazard in the runtime is worked around on purpose. For some syntax errors
its `ParserInterpreter` creates a token of its own and keeps it in a single
slot; the next such error overwrites the slot and frees the old token, while
the error node of the first error still points at it. Reading that node later is
a use-after-free, which crashes some of the time (typically on inputs with
several syntax errors). antlrlab therefore copies each error node's data while
its token is still valid (`ErrorNodeRecorder` in `src/analysis.hpp`) and never
touches those tokens afterwards.
**First step: `antlrlab --selftest`.** It compiles a tiny built-in grammar and
parses an input with it. If it passes, the installation is fine and the problem
is specific to your grammar or input. If it fails, the tool and runtime are not
working together: check that the jar and the runtime are the same version
(`antlrlab --version` shows what it was built for) and rebuild from scratch.
**To report a crash**, send the whole terminal output above, the grammar, the
input (or a smaller input that still crashes), your operating system and
compiler, and the output of `antlrlab --selftest`.
**To debug it yourself**, run without the child process so that the debugger
sees the crash:
```sh
gdb --args ./antlrlab --no-isolate Grammar.g4 input.txt # then: run, bt
lldb -- ./antlrlab --no-isolate Grammar.g4 input.txt # then: run, bt
```
Windows has no `fork`, so there the runtime runs in the main process, on a
thread with a 512 MB stack (falling back to smaller ones) that antlrlab creates
itself. A crash ends the program with exit status 2, but it is explained first:
the terminal shows the exception, what antlrlab was doing and a stack trace
(function names need debug symbols; without them you get `module+offset`).
`--timeout` and `--no-isolate` have no effect on Windows.
## Limitations
- Java is required at run time, because the grammar compiler exists only in Java.
- Tool and runtime versions must match. A mismatch shows up as a message that the
grammar could not be loaded.
- Names that clash with C++ (a rule called `int`, an alternative label `# Int` is
fine but `# int` is not) can make the tool report errors, because it generates
C++. antlrlab carries on when the tool only complains about such clashes and
still wrote its `.interp` files; otherwise rename the rule.
- Very large trees (more than 20,000 nodes) skip the diagram; the tree text and
tokens are still produced.
- A run that takes longer than `--timeout` (default 120 seconds) is stopped
(not on Windows).
- Parse trees deeper than 50,000 levels are cut off (with a warning), and the
indented tree text stops indenting after 40 levels.
- The web UI was exercised in Chromium. Firefox and Safari should work (it uses
only standard features) but have not been tried.
- Tested: Linux with GCC 13 and Clang 18 (the headers also compile with libc++),
and the Windows build cross-compiled with MinGW-w64 GCC 13 and run under Wine
with a Windows JDK (self-test, the examples, Unicode paths, the web UI, crash
reports). **Not yet run:** Visual C++, real Windows and macOS. They are covered
by the CI workflow (`.github/workflows/build.yml`); push to GitHub and read its
results, or build locally as described above.
- On Windows the runtime is not isolated in a child process (see
[If it crashes](#if-it-crashes)), so a crash or an endless run takes antlrlab
with it. Run the web UI with a grammar you trust, or on Linux, if that matters.
## Layout
```
src/platform.hpp everything OS-specific: UTF-8 paths, arguments, processes, console, random
src/main.cpp command line, console output, watch mode
src/antlr_tool.hpp finds and runs the ANTLR tool
src/interp.hpp reads .interp files
src/analysis.hpp the only file that uses the ANTLR runtime
src/render.hpp tree text and SVG layout
src/report.hpp report.html (HTML, CSS and JavaScript)
src/pipeline.hpp verdict and statistics shared by CLI and web UI
src/isolate.hpp runs the runtime in a child process on a big stack, with a time limit
src/crash.hpp crash reports: phase and stack trace (signals on POSIX, SEH + DbgHelp on Windows)
src/http.hpp small loopback HTTP server
src/json.hpp JSON parser for request bodies
src/serve.hpp web UI backend: files, run, security checks
src/webui_assets.hpp generated from web/ by tools/embed_web.py
web/ the web UI (HTML, CSS, JavaScript)
examples/ combined, split and JSON grammars with inputs
```
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
mkdir build
cmake -B build -DANTLRLAB_DOWNLOAD_JAR=ON
cmake --build build
+15
View File
@@ -0,0 +1,15 @@
# Cross-compiling for 64-bit Windows from Linux with MinGW-w64:
#
# sudo apt install g++-mingw-w64-x86-64-posix
# cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake -DANTLRLAB_DOWNLOAD_JAR=ON
# cmake --build build-win
#
# The "posix" thread model is required: the "win32" one has no std::thread.
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+21
View File
@@ -0,0 +1,21 @@
// A small expression grammar in one combined file.
// antlrlab examples/combined/Expr.g4 examples/combined/input.txt
grammar Expr;
prog : expr EOF ;
expr
: expr op=(MUL | DIV) expr # MulDiv
| expr op=(ADD | SUB) expr # AddSub
| INT # Int
| ID # Id
| '(' expr ')' # Parens
;
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
INT : [0-9]+ ;
ID : [a-zA-Z_] [a-zA-Z_0-9]* ;
WS : [ \t\r\n]+ -> skip ;
+1
View File
@@ -0,0 +1 @@
1 + 2 * (width - 3)
+1
View File
@@ -0,0 +1 @@
1 + * 2 (width
+31
View File
@@ -0,0 +1,31 @@
// JSON, straight from the ANTLR book. Try it on a .json file:
// antlrlab examples/json/JSON.g4 examples/json/example.json
grammar JSON;
json : value EOF ;
obj : '{' pair (',' pair)* '}'
| '{' '}'
;
pair : STRING ':' value ;
arr : '[' value (',' value)* ']'
| '[' ']'
;
value : STRING | NUMBER | obj | arr | 'true' | 'false' | 'null' ;
STRING : '"' (ESC | SAFECODEPOINT)* '"' ;
fragment ESC : '\\' (["\\/bfnrt] | UNICODE) ;
fragment UNICODE : 'u' HEX HEX HEX HEX ;
fragment HEX : [0-9a-fA-F] ;
fragment SAFECODEPOINT : ~["\\\u0000-\u001F] ;
NUMBER : '-'? INT ('.' [0-9]+)? EXP? ;
fragment INT : '0' | [1-9] [0-9]* ;
fragment EXP : [Ee] [+\-]? INT ;
WS : [ \t\n\r]+ -> skip ;
+8
View File
@@ -0,0 +1,8 @@
{
"name": "antlrlab",
"tags": ["parser", "antlr", "diagram"],
"version": 1.0,
"stable": true,
"owner": null,
"nested": { "depth": 2, "items": [1, 2, 3] }
}
+18
View File
@@ -0,0 +1,18 @@
// Lexer and parser in separate files.
// antlrlab examples/split/CalcLexer.g4 examples/split/CalcParser.g4 examples/split/input.txt
lexer grammar CalcLexer;
PLUS : '+' ;
MINUS : '-' ;
STAR : '*' ;
SLASH : '/' ;
LPAREN : '(' ;
RPAREN : ')' ;
ASSIGN : '=' ;
SEMI : ';' ;
NUMBER : [0-9]+ ('.' [0-9]+)? ;
IDENT : [a-zA-Z_] [a-zA-Z_0-9]* ;
LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN) ;
WS : [ \t\r\n]+ -> skip ;
+15
View File
@@ -0,0 +1,15 @@
parser grammar CalcParser;
options { tokenVocab = CalcLexer; }
program : statement* EOF ;
statement : IDENT ASSIGN expression SEMI ;
expression
: expression (STAR | SLASH) expression
| expression (PLUS | MINUS) expression
| LPAREN expression RPAREN
| NUMBER
| IDENT
;
+3
View File
@@ -0,0 +1,3 @@
// two statements
width = 4;
area = width * (width + 2.5) - 1;
+487
View File
@@ -0,0 +1,487 @@
// The only file that talks to the ANTLR C++ runtime.
//
// It loads the .interp data written by the ANTLR tool, then runs the lexer
// and parser through the runtime's interpreters (LexerInterpreter,
// ParserInterpreter). Everything is copied into plain lab:: types before the
// runtime objects go away.
//
// Written against the ANTLR 4.13 C++ runtime API.
#pragma once
#include "util.hpp" // must come first: it pulls in <cstdio> before the runtime undefines EOF
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <exception>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "antlr4-runtime.h"
#include "crash.hpp"
#include "interp.hpp"
#include "types.hpp"
#ifndef LAB_ANTLR_VERSION
#define LAB_ANTLR_VERSION "4.13.2"
#endif
namespace lab {
// ------------------------------------------------------------------ model ----
struct Model {
InterpData lexer;
InterpData parser;
bool hasParser = false;
std::unique_ptr<antlr4::atn::ATN> lexerAtn;
std::unique_ptr<antlr4::atn::ATN> parserAtn;
std::unique_ptr<antlr4::dfa::Vocabulary> lexVocab;
std::unique_ptr<antlr4::dfa::Vocabulary> parVocab;
std::string tokenName(size_t type) const {
if (type == antlr4::Token::EOF) return "EOF";
if (type == 0) return "invalid"; // Token::INVALID_TYPE, used for error nodes
auto from = [&](const InterpData& d) -> std::string {
if (type < d.symbolicNames.size() && !d.symbolicNames[type].empty()) return d.symbolicNames[type];
if (type < d.literalNames.size() && !d.literalNames[type].empty()) return d.literalNames[type];
return "";
};
std::string n = from(lexer);
if (n.empty() && hasParser) n = from(parser);
return n.empty() ? std::to_string(type) : n;
}
std::string channelName(size_t ch) const {
if (ch < lexer.channelNames.size()) return lexer.channelNames[ch];
return std::to_string(ch);
}
};
// What the .interp files say, with no ANTLR runtime object created yet. Safe
// to build and keep in any process; the runtime is only touched by
// buildModel(), which the callers run in an isolated worker (isolate.hpp).
struct ModelSpec {
InterpData lexer;
InterpData parser;
bool hasParser = false;
};
// Reads the given .interp files: exactly one lexer, at most one parser.
inline bool loadInterps(const std::vector<fs::path>& interpFiles, ModelSpec& spec, std::string& error) {
std::vector<InterpData> lexers, parsers;
for (const fs::path& f : interpFiles) {
std::string text;
if (!readFile(f, text)) {
error = "cannot read " + pathToUtf8(f);
return false;
}
InterpData d;
d.name = pathToUtf8(f.stem());
std::string err;
if (!parseInterp(text, d, err)) {
error = pathToUtf8(f.filename()) + ": " + err;
return false;
}
(d.isLexer ? lexers : parsers).push_back(std::move(d));
}
if (lexers.empty()) {
error = "no lexer found. Give the program a combined grammar, or a lexer grammar together "
"with the parser grammar that uses it.";
return false;
}
if (lexers.size() > 1 || parsers.size() > 1) {
error = "more than one lexer or parser was generated. Pass one grammar, or one lexer plus "
"one parser grammar.";
return false;
}
spec.lexer = std::move(lexers.front());
spec.hasParser = !parsers.empty();
if (spec.hasParser) spec.parser = std::move(parsers.front());
return true;
}
// Creates the runtime objects (the ATNs) from the spec and checks that they
// agree with the rule names, because the interpreters index one by the other.
inline bool buildModel(const ModelSpec& spec, Model& m, std::string& error) {
try {
m.lexer = spec.lexer;
m.hasParser = spec.hasParser;
if (m.hasParser) m.parser = spec.parser;
antlr4::atn::ATNDeserializer deserializer;
crash::setPhase("loading the lexer ATN into the ANTLR runtime");
m.lexerAtn = deserializer.deserialize(
antlr4::atn::SerializedATNView(m.lexer.atn.data(), m.lexer.atn.size()));
if (!m.lexerAtn || m.lexerAtn->grammarType != antlr4::atn::ATNType::LEXER) {
error = m.lexer.name + ".interp does not contain a lexer ATN.";
return false;
}
if (m.lexerAtn->ruleToStartState.size() != m.lexer.ruleNames.size()) {
error = m.lexer.name + ".interp is inconsistent: " + std::to_string(m.lexer.ruleNames.size()) +
" rule names but the ATN has " + std::to_string(m.lexerAtn->ruleToStartState.size()) + " rules.";
return false;
}
m.lexVocab = std::make_unique<antlr4::dfa::Vocabulary>(m.lexer.literalNames, m.lexer.symbolicNames);
if (m.hasParser) {
crash::setPhase("loading the parser ATN into the ANTLR runtime");
m.parserAtn = deserializer.deserialize(
antlr4::atn::SerializedATNView(m.parser.atn.data(), m.parser.atn.size()));
if (!m.parserAtn || m.parserAtn->grammarType != antlr4::atn::ATNType::PARSER) {
error = m.parser.name + ".interp does not contain a parser ATN.";
return false;
}
if (m.parserAtn->ruleToStartState.size() != m.parser.ruleNames.size()) {
error = m.parser.name + ".interp is inconsistent: " + std::to_string(m.parser.ruleNames.size()) +
" rule names but the ATN has " + std::to_string(m.parserAtn->ruleToStartState.size()) + " rules.";
return false;
}
m.parVocab = std::make_unique<antlr4::dfa::Vocabulary>(m.parser.literalNames, m.parser.symbolicNames);
}
return true;
} catch (const std::exception& e) {
error = std::string("could not load the grammar into the ANTLR runtime: ") + e.what() +
"\n(The ANTLR tool and the C++ runtime must be the same version. This program was "
"built for " LAB_ANTLR_VERSION ".)";
return false;
}
}
// --------------------------------------------------------------- analysis ----
namespace detail {
inline int64_t toIndex(size_t v) {
return v == static_cast<size_t>(-1) ? -1 : static_cast<int64_t>(v);
}
// Turns (line, column) and offending-symbol information into diagnostics.
class CollectingListener : public antlr4::BaseErrorListener {
public:
CollectingListener(std::vector<Diagnostic>& out, const LineTable& lines, bool lexer)
: out_(out), lines_(lines), lexer_(lexer) {}
size_t count() const { return count_; }
void syntaxError(antlr4::Recognizer* recognizer, antlr4::Token* offendingSymbol, size_t line,
size_t charPositionInLine, const std::string& msg, std::exception_ptr) override {
Diagnostic d;
d.severity = Diagnostic::Severity::Error;
d.source = lexer_ ? "lexer" : "parser";
d.line = line;
d.col = charPositionInLine;
d.message = msg;
if (lexer_) {
d.start = lines_.indexOf(line, charPositionInLine);
d.stop = d.start;
if (auto* lx = dynamic_cast<antlr4::Lexer*>(recognizer)) {
if (antlr4::CharStream* cs = lx->getInputStream()) {
int64_t e = static_cast<int64_t>(cs->index());
e = std::min(e, lines_.total() - 1);
if (d.start >= 0 && e > d.stop) d.stop = e;
}
}
} else if (offendingSymbol != nullptr && offendingSymbol->getType() != antlr4::Token::EOF) {
d.start = toIndex(offendingSymbol->getStartIndex());
d.stop = toIndex(offendingSymbol->getStopIndex());
if (d.start < 0 || d.stop < d.start) d.start = d.stop = -1;
}
out_.push_back(std::move(d));
++count_;
}
private:
std::vector<Diagnostic>& out_;
const LineTable& lines_;
bool lexer_;
size_t count_ = 0;
};
// What an error node says, copied out of its token.
struct ErrorNodeInfo {
std::string text;
std::string typeName;
size_t line = 0;
size_t col = 0;
int64_t start = -1;
int64_t stop = -1;
};
// Copies every error node's data out of its token while that token is still
// valid, and nothing else may read those tokens afterwards.
//
// Why: for some syntax errors ParserInterpreter creates a token of its own
// and keeps it in a single slot. The next such error overwrites the slot and
// frees the old token, but the error node of the first error still points at
// it. Reading that node's token later is a use-after-free: it often "works"
// and sometimes crashes. The rule that contains an error node always ends
// right after the error, before another one can happen, so the node's token
// is still alive when exitEveryRule() runs for that rule.
//
// Both parameter types are declared for visitTerminal and visitErrorNode
// (without `override`) so that this compiles against runtime versions that
// differ in the exact signature; whichever one the base class declares is the
// one that gets overridden.
class ErrorNodeRecorder : public antlr4::tree::ParseTreeListener {
public:
explicit ErrorNodeRecorder(const Model& m) : m_(m) {}
void visitTerminal(antlr4::tree::TerminalNode*) {}
void visitTerminal(antlr4::tree::ParseTree*) {}
void visitErrorNode(antlr4::tree::ErrorNode* node) { record(node); }
void visitErrorNode(antlr4::tree::ParseTree* node) {
if (auto* e = dynamic_cast<antlr4::tree::ErrorNode*>(node)) record(e);
}
void enterEveryRule(antlr4::ParserRuleContext*) {}
void exitEveryRule(antlr4::ParserRuleContext* ctx) {
if (ctx == nullptr) return;
for (antlr4::tree::ParseTree* child : ctx->children) {
if (auto* e = dynamic_cast<antlr4::tree::ErrorNode*>(child)) record(e);
}
}
const ErrorNodeInfo* find(const antlr4::tree::ParseTree* node) const {
const auto it = infos_.find(node);
return it == infos_.end() ? nullptr : &it->second;
}
private:
void record(antlr4::tree::ErrorNode* node) {
const antlr4::tree::ParseTree* key = node;
if (infos_.count(key) != 0) return; // already copied; its token may be gone by now
ErrorNodeInfo info;
if (antlr4::Token* tok = node->getSymbol()) {
info.text = tok->getText();
info.typeName = m_.tokenName(tok->getType());
info.line = tok->getLine();
info.col = tok->getCharPositionInLine();
info.start = toIndex(tok->getStartIndex());
info.stop = toIndex(tok->getStopIndex());
if (info.start < 0 || info.stop < info.start) info.start = info.stop = -1;
}
infos_.emplace(key, std::move(info));
}
const Model& m_;
std::unordered_map<const antlr4::tree::ParseTree*, ErrorNodeInfo> infos_;
};
// Deeper parse trees than this are cut off (and reported). Every recursive
// function in the program is bounded by it.
constexpr size_t kMaxTreeDepth = 50000;
inline void convertTree(antlr4::tree::ParseTree* t, const Model& m, const ErrorNodeRecorder& errors, Node& out,
size_t& count, size_t depth, bool& truncated) {
++count;
if (depth > kMaxTreeDepth) {
out.kind = Node::Kind::Terminal;
out.label = "\u2026";
out.typeName = "depth limit";
truncated = true;
return;
}
auto fillToken = [&](antlr4::Token* tok) {
if (tok == nullptr) return;
out.label = tok->getText();
out.typeName = m.tokenName(tok->getType());
out.line = tok->getLine();
out.col = tok->getCharPositionInLine();
out.start = toIndex(tok->getStartIndex());
out.stop = toIndex(tok->getStopIndex());
if (out.start < 0 || out.stop < out.start) out.start = out.stop = -1;
};
// ErrorNode derives from TerminalNode, so it has to be tested first. Its
// token must not be touched here (see ErrorNodeRecorder): use the copy.
if (dynamic_cast<antlr4::tree::ErrorNode*>(t) != nullptr) {
out.kind = Node::Kind::Error;
if (const ErrorNodeInfo* info = errors.find(t)) {
out.label = info->text;
out.typeName = info->typeName;
out.line = info->line;
out.col = info->col;
out.start = info->start;
out.stop = info->stop;
} else {
out.label = "<error>";
out.typeName = "error";
}
return;
}
if (auto* term = dynamic_cast<antlr4::tree::TerminalNode*>(t)) {
out.kind = Node::Kind::Terminal;
fillToken(term->getSymbol());
return;
}
out.kind = Node::Kind::Rule;
auto* ctx = dynamic_cast<antlr4::ParserRuleContext*>(t);
if (ctx == nullptr) {
out.label = "?";
return;
}
const size_t rule = ctx->getRuleIndex();
out.label = rule < m.parser.ruleNames.size() ? m.parser.ruleNames[rule] : "rule" + std::to_string(rule);
out.children.reserve(ctx->children.size());
for (antlr4::tree::ParseTree* c : ctx->children) {
out.children.emplace_back();
convertTree(c, m, errors, out.children.back(), count, depth + 1, truncated);
}
for (const Node& c : out.children) { // the rule spans its children
if (c.start < 0) continue;
if (out.start < 0 || c.start < out.start) out.start = c.start;
if (c.stop > out.stop) out.stop = c.stop;
}
if (out.start < 0) { // empty or all-error rule: fall back to the context's tokens
const int64_t s = ctx->getStart() ? toIndex(ctx->getStart()->getStartIndex()) : -1;
const int64_t e = ctx->getStop() ? toIndex(ctx->getStop()->getStopIndex()) : -1;
if (s >= 0 && e >= s) {
out.start = s;
out.stop = e;
}
}
if (out.stop < out.start) out.start = out.stop = -1;
}
} // namespace detail
// Runs lexer (and parser, when there is one) over `text`, which must already
// be sanitized UTF-8. `startRule` may be empty (= first parser rule).
// Returns false only for problems with the request itself (unknown start rule).
inline bool analyze(const Model& m, const std::string& text, const std::string& startRule,
AnalysisResult& out, std::string& error) {
out = AnalysisResult();
size_t startIdx = 0;
if (m.hasParser) {
const auto& names = m.parser.ruleNames;
if (names.empty()) {
error = "the parser grammar has no rules";
return false;
}
if (!startRule.empty()) {
const auto it = std::find(names.begin(), names.end(), startRule);
if (it == names.end()) {
error = "unknown start rule '" + startRule + "'. Parser rules: ";
for (size_t i = 0; i < names.size(); ++i) error += (i ? ", " : "") + names[i];
return false;
}
startIdx = static_cast<size_t>(it - names.begin());
}
out.startRule = names[startIdx];
}
const LineTable lines(text);
using Clock = std::chrono::steady_clock;
auto ms = [](Clock::time_point a, Clock::time_point b) {
return std::chrono::duration<double, std::milli>(b - a).count();
};
try {
detail::CollectingListener lexErrors(out.diagnostics, lines, true);
detail::CollectingListener parseErrors(out.diagnostics, lines, false);
crash::setPhase("creating the lexer");
antlr4::ANTLRInputStream input(text);
antlr4::LexerInterpreter lexer(m.lexer.name, *m.lexVocab, m.lexer.ruleNames, m.lexer.channelNames,
m.lexer.modeNames, *m.lexerAtn, &input);
lexer.removeErrorListeners();
lexer.addErrorListener(&lexErrors);
antlr4::CommonTokenStream tokens(&lexer);
crash::setPhase("splitting the input into tokens (lexer)");
const auto t0 = Clock::now();
tokens.fill();
out.lexMs = ms(t0, Clock::now());
crash::setPhase("reading the tokens");
for (antlr4::Token* t : tokens.getTokens()) {
TokenInfo ti;
ti.index = t->getTokenIndex();
ti.text = t->getText();
ti.typeName = m.tokenName(t->getType());
ti.channel = m.channelName(t->getChannel());
ti.line = t->getLine();
ti.col = t->getCharPositionInLine();
ti.isEof = t->getType() == antlr4::Token::EOF;
if (!ti.isEof) {
ti.start = detail::toIndex(t->getStartIndex());
ti.stop = detail::toIndex(t->getStopIndex());
}
out.tokens.push_back(std::move(ti));
}
if (m.hasParser) {
crash::setPhase("creating the parser");
detail::ErrorNodeRecorder errorNodes(m); // declared before the parser, destroyed after it
antlr4::ParserInterpreter parser(m.parser.name, *m.parVocab, m.parser.ruleNames, *m.parserAtn,
&tokens);
parser.removeErrorListeners();
parser.addErrorListener(&parseErrors);
parser.addParseListener(&errorNodes);
tokens.seek(0);
crash::setPhase("parsing the input (parser)");
const auto t1 = Clock::now();
antlr4::ParserRuleContext* root = parser.parse(startIdx);
out.parseMs = ms(t1, Clock::now());
if (root != nullptr) {
crash::setPhase("copying the parse tree");
bool truncated = false;
detail::convertTree(root, m, errorNodes, out.tree, out.nodeCount, 0, truncated);
out.hasTree = true;
if (truncated) {
Diagnostic d;
d.severity = Diagnostic::Severity::Warning;
d.source = "parser";
d.message = "the parse tree is more than " + std::to_string(detail::kMaxTreeDepth) +
" levels deep; everything below that is not shown.";
out.diagnostics.push_back(std::move(d));
}
}
// The lab does not force EOF either: a start rule may stop early. Say so.
if (parseErrors.count() == 0 && tokens.LA(1) != antlr4::Token::EOF) {
antlr4::Token* next = tokens.LT(1);
Diagnostic d;
d.severity = Diagnostic::Severity::Warning;
d.source = "parser";
if (next != nullptr) {
d.line = next->getLine();
d.col = next->getCharPositionInLine();
d.start = detail::toIndex(next->getStartIndex());
d.stop = detail::toIndex(next->getStopIndex());
if (d.start < 0 || d.stop < d.start) d.start = d.stop = -1;
}
d.message = "rule '" + out.startRule + "' matched only part of the input" +
(next ? " and stopped at '" + visible(next->getText()) + "'" : "") +
". End the rule with EOF to require a full match.";
out.diagnostics.push_back(std::move(d));
}
}
} catch (const std::exception& e) {
Diagnostic d;
d.source = "input";
d.message = std::string("internal error while running the grammar: ") + e.what();
out.diagnostics.push_back(std::move(d));
out.hasTree = false;
}
crash::setPhase("finishing");
std::stable_sort(out.diagnostics.begin(), out.diagnostics.end(),
[](const Diagnostic& a, const Diagnostic& b) {
if (a.line != b.line) return a.line < b.line;
return a.col < b.col;
});
return true;
}
} // namespace lab
+338
View File
@@ -0,0 +1,338 @@
// Finds the ANTLR tool (a Java program) and runs it on the user's grammars.
//
// The C++ runtime cannot turn a .g4 file into something runnable by itself, so
// we ask the Java tool to "generate" C++ and only keep the .interp files it
// writes next to the generated code. Those files contain everything the
// runtime's interpreters need (see interp.hpp).
#pragma once
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <string>
#include <vector>
#include "util.hpp"
#ifndef LAB_DEFAULT_ANTLR_JAR
#define LAB_DEFAULT_ANTLR_JAR ""
#endif
#ifndef LAB_ANTLR_VERSION
#define LAB_ANTLR_VERSION "4.13.2"
#endif
namespace lab {
struct Tool {
bool found = false;
std::vector<std::string> argv; // program and leading arguments, e.g. {"java", "-jar", "/x/antlr.jar"}
std::string description; // for messages
std::string error; // set when !found
};
inline std::string envVar(const char* name) { return getEnv(name); }
inline bool isRegularFile(const fs::path& p) {
std::error_code ec;
return fs::is_regular_file(p, ec);
}
inline bool isRegularFile(const std::string& utf8Path) { return isRegularFile(pathFromUtf8(utf8Path)); }
inline bool isDirectory(const fs::path& p) {
std::error_code ec;
return fs::is_directory(p, ec);
}
inline bool isDirectory(const std::string& utf8Path) { return isDirectory(pathFromUtf8(utf8Path)); }
#ifdef _WIN32
constexpr char kPathSep = ';';
#else
constexpr char kPathSep = ':';
#endif
inline std::vector<std::string> splitPathList(const std::string& s) {
std::vector<std::string> out;
size_t i = 0;
while (i <= s.size()) {
const size_t j = s.find(kPathSep, i);
const std::string part = s.substr(i, j == std::string::npos ? std::string::npos : j - i);
if (!part.empty()) out.push_back(part);
if (j == std::string::npos) break;
i = j + 1;
}
return out;
}
// Returns the full path of an executable found on PATH, or "".
inline std::string findOnPath(const std::string& name) {
static const char* const kExts[] = {
#ifdef _WIN32
".exe", ".cmd", ".bat" // a file without an extension cannot be run on Windows
#else
""
#endif
};
for (const std::string& dir : splitPathList(envVar("PATH"))) {
for (const char* ext : kExts) {
const fs::path p = pathFromUtf8(dir) / pathFromUtf8(name + ext);
if (isRegularFile(p)) return pathToUtf8(p);
}
}
return "";
}
// "java" from JAVA_HOME or PATH.
inline bool javaCommand(std::string& program, std::string& error) {
const std::string home = envVar("JAVA_HOME");
if (!home.empty()) {
fs::path p = pathFromUtf8(home) / "bin" / "java";
#ifdef _WIN32
p += ".exe";
#endif
if (isRegularFile(p)) {
program = pathToUtf8(p);
return true;
}
}
const std::string onPath = findOnPath("java");
if (!onPath.empty()) {
program = onPath;
return true;
}
error = "Java was not found. The ANTLR tool is a Java program: install a JDK/JRE (11 or newer) "
"or set JAVA_HOME.";
return false;
}
// ------------------------------------------------------------ jar search ----
inline std::string lowerCase(std::string s) {
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
}
inline bool looksLikeAntlrJar(const fs::path& p) {
const std::string n = lowerCase(pathToUtf8(p.filename()));
return n.rfind("antlr", 0) == 0 && n.size() > 4 && n.compare(n.size() - 4, 4, ".jar") == 0 &&
n.find("complete") != std::string::npos;
}
inline std::vector<int> versionNumbers(const std::string& s) {
std::vector<int> v;
for (size_t i = 0; i < s.size();) {
if (s[i] >= '0' && s[i] <= '9') {
int n = 0;
while (i < s.size() && s[i] >= '0' && s[i] <= '9') n = n * 10 + (s[i++] - '0');
v.push_back(n);
} else {
++i;
}
}
return v;
}
// Looks in the usual places for antlr-X.Y.Z-complete.jar. Prefers the version
// this program was built against, otherwise the newest one.
inline std::string searchJar() {
std::vector<fs::path> dirs;
std::vector<fs::path> files;
std::error_code ec;
dirs.push_back(fs::current_path(ec));
const fs::path exe = executablePath();
if (!exe.empty()) dirs.push_back(exe.parent_path());
for (const std::string& e : splitPathList(envVar("CLASSPATH"))) {
const fs::path p = pathFromUtf8(e);
if (isDirectory(p)) dirs.push_back(p);
else if (looksLikeAntlrJar(p)) files.push_back(p);
}
const std::string home = envVar("HOME").empty() ? envVar("USERPROFILE") : envVar("HOME");
if (!home.empty()) {
dirs.push_back(pathFromUtf8(home));
dirs.push_back(pathFromUtf8(home) / ".m2");
}
#ifdef _WIN32
for (const char* d : {"C:\\Javalib", "C:\\antlr", "C:\\Program Files\\ANTLR", "C:\\Program Files\\Java"}) {
dirs.emplace_back(d);
}
#else
for (const char* d : {"/usr/local/lib", "/usr/lib", "/usr/share/java", "/opt/homebrew/lib",
"/opt/homebrew/opt/antlr/libexec", "/usr/local/opt/antlr/libexec"}) {
dirs.emplace_back(d);
}
#endif
for (const fs::path& d : dirs) {
if (!isDirectory(d)) continue;
for (fs::directory_iterator it(d, fs::directory_options::skip_permission_denied, ec), end;
!ec && it != end; it.increment(ec)) {
if (it->is_regular_file(ec) && looksLikeAntlrJar(it->path())) files.push_back(it->path());
}
ec.clear();
}
if (files.empty()) return "";
const std::string want = LAB_ANTLR_VERSION;
auto rank = [&](const fs::path& p) {
return pathToUtf8(p.filename()).find(want) != std::string::npos ? 1 : 0;
};
std::stable_sort(files.begin(), files.end(), [&](const fs::path& a, const fs::path& b) {
if (rank(a) != rank(b)) return rank(a) > rank(b);
return versionNumbers(pathToUtf8(a.filename())) > versionNumbers(pathToUtf8(b.filename()));
});
return pathToUtf8(files.front());
}
// ---------------------------------------------------------------- locate ----
// Order: --antlr-jar, $ANTLR_JAR, compile-time default, jar search, then an
// "antlr4" / "antlr" launcher on PATH.
inline Tool locateTool(const std::string& explicitJar) {
Tool t;
auto useJar = [&](const std::string& jar, const std::string& how) {
std::string java, err;
if (!javaCommand(java, err)) {
t.error = err;
return;
}
t.found = true;
t.argv = {java, "-jar", toolSafePath(jar)};
t.description = jar + how;
};
if (!explicitJar.empty()) {
if (!isRegularFile(explicitJar)) t.error = "ANTLR jar not found: " + explicitJar;
else useJar(explicitJar, "");
return t;
}
const std::string envJar = envVar("ANTLR_JAR");
if (!envJar.empty()) {
if (!isRegularFile(envJar)) t.error = "ANTLR_JAR points to a file that does not exist: " + envJar;
else useJar(envJar, " (from ANTLR_JAR)");
return t;
}
const std::string builtIn = LAB_DEFAULT_ANTLR_JAR;
if (!builtIn.empty() && isRegularFile(builtIn)) {
useJar(builtIn, "");
return t;
}
const std::string found = searchJar();
if (!found.empty()) {
useJar(found, "");
return t;
}
for (const char* name : {"antlr4", "antlr"}) {
const std::string p = findOnPath(name);
if (!p.empty()) {
t.found = true;
t.argv = {p};
t.description = p;
return t;
}
}
t.error =
"Could not find the ANTLR tool.\n"
" - pass --antlr-jar /path/to/antlr-" LAB_ANTLR_VERSION "-complete.jar, or\n"
" - set the ANTLR_JAR environment variable, or\n"
" - put antlr-" LAB_ANTLR_VERSION "-complete.jar in the current directory, or\n"
" - configure the build with -DANTLRLAB_DOWNLOAD_JAR=ON to fetch it.";
return t;
}
// --------------------------------------------------------------- compile ----
struct CompileResult {
bool ok = false;
int exitCode = 0;
std::string log; // combined stdout + stderr of the tool
std::string command; // what was run, for display
std::vector<fs::path> interps;
std::string summary; // one line for the failure case
};
inline std::string trimLine(std::string s) {
while (!s.empty() && (s.back() == '\r' || s.back() == ' ' || s.back() == '\t')) s.pop_back();
size_t i = 0;
while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
return s.substr(i);
}
// Some "errors" only concern the generated C++ (a rule named "int", say).
// We never compile that code, so they are harmless for interpreting.
inline bool onlyGeneratedCodeErrors(const std::string& log) {
size_t tolerated = 0, other = 0;
std::istringstream in(log);
std::string line;
while (std::getline(in, line)) {
line = trimLine(line);
if (line.rfind("error(", 0) != 0) continue;
if (line.rfind("error(134)", 0) == 0 || line.find("conflicts with generated code") != std::string::npos) {
++tolerated;
} else {
++other;
}
}
return tolerated > 0 && other == 0;
}
inline std::string firstProblemLine(const std::string& log) {
std::istringstream in(log);
std::string line, firstAny;
while (std::getline(in, line)) {
line = trimLine(line);
if (line.empty()) continue;
if (line.rfind("error(", 0) == 0) return line;
if (firstAny.empty()) firstAny = line;
}
return firstAny;
}
inline CompileResult compileGrammars(const Tool& tool, const std::vector<fs::path>& grammars,
const std::string& libDir, const fs::path& genDir,
const fs::path& logFile) {
CompileResult r;
std::error_code ec;
fs::remove_all(genDir, ec);
fs::create_directories(genDir, ec);
if (ec) {
r.summary = "cannot create " + pathToUtf8(genDir) + ": " + ec.message();
return r;
}
std::vector<std::string> argv = tool.argv;
for (const char* a : {"-Dlanguage=Cpp", "-no-listener", "-no-visitor", "-Xexact-output-dir", "-o"}) argv.emplace_back(a);
argv.push_back(toolSafePath(pathToUtf8(genDir)));
if (!libDir.empty()) {
argv.emplace_back("-lib");
argv.push_back(toolSafePath(libDir));
}
for (const fs::path& g : grammars) argv.push_back(toolSafePath(pathToUtf8(g)));
for (const std::string& a : argv) r.command += (r.command.empty() ? "" : " ") + a;
const ProcessResult pr = runProcess(argv, logFile);
r.exitCode = pr.exitCode;
if (pr.started) readFile(logFile, r.log);
else r.log = "could not start " + argv[0] + ": " + pr.error + "\n";
for (fs::recursive_directory_iterator it(genDir, ec), end; !ec && it != end; it.increment(ec)) {
if (it->is_regular_file(ec) && it->path().extension() == ".interp") r.interps.push_back(it->path());
}
std::sort(r.interps.begin(), r.interps.end());
const bool clean = r.exitCode == 0;
if ((clean || onlyGeneratedCodeErrors(r.log)) && !r.interps.empty()) {
r.ok = true;
return r;
}
r.summary = firstProblemLine(r.log);
if (r.summary.empty()) {
if (!pr.started) r.summary = "could not start the ANTLR tool (" + pr.error + ")";
else if (r.exitCode == 127 || r.exitCode == -1) r.summary = "could not start the ANTLR tool";
else if (clean) r.summary = "the ANTLR tool wrote no .interp files (is it older than 4.7?)";
else r.summary = "the ANTLR tool exited with code " + std::to_string(r.exitCode);
}
return r;
}
} // namespace lab
+351
View File
@@ -0,0 +1,351 @@
// Crash reporting. If the ANTLR runtime (or this program) dies with a fatal
// signal, say what we were doing and print a stack trace instead of the bare
// "Segmentation fault" the shell gives.
//
// crash::setPhase("parsing the input") -- cheap; call it before risky work
// crash::installHandlers() -- once, at start-up
// crash::AltStack -- one per thread that may overflow
//
// The phase lives in shared memory, so a parent process can still read it
// after a forked child has died (see isolate.hpp).
#pragma once
#include "platform.hpp"
#include <atomic>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#if defined(__unix__) || defined(__APPLE__)
#define LAB_POSIX 1
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
#if defined(__has_include)
#if __has_include(<execinfo.h>)
#include <execinfo.h>
#define LAB_HAVE_BACKTRACE 1
#endif
#endif
#elif defined(_WIN32)
#include <dbghelp.h> // after windows.h (platform.hpp)
#endif
namespace lab {
namespace crash {
struct Shared {
char phase[120];
};
inline Shared*& sharedSlot() {
static Shared* p = nullptr;
return p;
}
inline Shared& shared() {
static Shared local = {};
Shared* p = sharedSlot();
return p ? *p : local;
}
// Call once before forking so that parent and child see the same memory.
inline void initShared() {
#ifdef LAB_POSIX
if (sharedSlot()) return;
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
void* m = ::mmap(nullptr, sizeof(Shared), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (m != MAP_FAILED) {
sharedSlot() = static_cast<Shared*>(m);
std::memset(sharedSlot(), 0, sizeof(Shared));
}
#endif
}
inline void setPhase(const char* what) {
Shared& s = shared();
size_t i = 0;
for (; what[i] != '\0' && i + 1 < sizeof s.phase; ++i) s.phase[i] = what[i];
s.phase[i] = '\0';
}
inline std::string phase() { return std::string(shared().phase); }
#ifdef LAB_POSIX
inline const char* signalName(int sig) {
switch (sig) {
case SIGSEGV: return "SIGSEGV (invalid memory access, often a stack overflow)";
case SIGBUS: return "SIGBUS (bus error)";
case SIGFPE: return "SIGFPE (arithmetic error)";
case SIGILL: return "SIGILL (illegal instruction)";
case SIGABRT: return "SIGABRT (abort, usually an uncaught exception or failed assertion)";
case SIGKILL: return "SIGKILL (killed, for example by the out-of-memory killer)";
case SIGTERM: return "SIGTERM";
case SIGINT: return "SIGINT";
default: return "a fatal signal";
}
}
inline void writeStr(const char* s) {
size_t n = std::strlen(s);
while (n > 0) {
const ssize_t w = ::write(2, s, n);
if (w <= 0) return;
s += w;
n -= static_cast<size_t>(w);
}
}
inline void writeHex(unsigned long v) {
char b[24];
int i = 23;
b[i] = '\0';
do {
b[--i] = "0123456789abcdef"[v & 15];
v >>= 4;
} while (v != 0 && i > 2);
b[--i] = 'x';
b[--i] = '0';
writeStr(b + i);
}
extern "C" inline void onFatalSignal(int sig, siginfo_t* info, void*) {
static std::atomic<int> entered{0};
if (entered.exchange(1) != 0) _exit(128 + sig);
writeStr("\n[antlrlab] fatal error: ");
writeStr(signalName(sig));
writeStr("\n[antlrlab] while: ");
writeStr(shared().phase[0] ? shared().phase : "(unknown)");
if (info && (sig == SIGSEGV || sig == SIGBUS)) {
writeStr("\n[antlrlab] address: ");
writeHex(reinterpret_cast<unsigned long>(info->si_addr));
}
#ifdef LAB_HAVE_BACKTRACE
writeStr("\n[antlrlab] stack trace (innermost first):\n");
void* frames[64];
const int n = ::backtrace(frames, 64);
::backtrace_symbols_fd(frames, n, 2);
#endif
writeStr("[antlrlab] This is a bug in antlrlab or in the ANTLR C++ runtime, not in your grammar.\n");
writeStr("[antlrlab] Please report it with the stack trace above, the grammar and the input.\n");
// SA_RESETHAND restored the default action: returning re-raises the signal.
}
inline void installHandlers() {
#ifdef LAB_HAVE_BACKTRACE
void* warm[2];
::backtrace(warm, 2); // loads the unwinder now, not inside a crashing process
#endif
struct sigaction sa;
std::memset(&sa, 0, sizeof sa);
sa.sa_sigaction = onFatalSignal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND;
for (int sig : {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGABRT}) ::sigaction(sig, &sa, nullptr);
}
// A separate stack for the handler, so that it can run after a stack overflow.
// The alternate stack belongs to one thread: create one of these in each
// thread that may overflow (the main thread, workers).
class AltStack {
public:
AltStack() {
stack_t ss;
mem_ = std::malloc(kSize);
if (!mem_) return;
ss.ss_sp = mem_;
ss.ss_size = kSize;
ss.ss_flags = 0;
if (::sigaltstack(&ss, nullptr) != 0) {
std::free(mem_);
mem_ = nullptr;
}
}
~AltStack() {
if (!mem_) return;
stack_t ss;
std::memset(&ss, 0, sizeof ss);
ss.ss_flags = SS_DISABLE;
::sigaltstack(&ss, nullptr);
std::free(mem_);
}
AltStack(const AltStack&) = delete;
AltStack& operator=(const AltStack&) = delete;
private:
static constexpr size_t kSize = 128 * 1024;
void* mem_ = nullptr;
};
#elif defined(_WIN32)
// Windows: no fork, so a crash cannot be survived, but it can be explained:
// what we were doing, the exception, and a stack trace. The process then
// ends with exit status 2.
inline const char* signalName(int) { return "a fatal error"; }
namespace detail {
inline void out(const char* s) {
DWORD written = 0;
::WriteFile(::GetStdHandle(STD_ERROR_HANDLE), s, static_cast<DWORD>(std::strlen(s)), &written, nullptr);
}
inline const char* exceptionName(DWORD code) {
switch (code) {
case EXCEPTION_ACCESS_VIOLATION: return "access violation (invalid memory access)";
case EXCEPTION_STACK_OVERFLOW: return "stack overflow";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "integer division by zero";
case EXCEPTION_ILLEGAL_INSTRUCTION: return "illegal instruction";
case EXCEPTION_IN_PAGE_ERROR: return "page error (invalid memory access)";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "array bounds exceeded";
case EXCEPTION_PRIV_INSTRUCTION: return "privileged instruction";
case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "non-continuable exception";
default: return "an unhandled exception";
}
}
inline void printStackTrace(const CONTEXT* faulting) {
HANDLE proc = ::GetCurrentProcess();
HANDLE thread = ::GetCurrentThread();
::SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
if (!::SymInitialize(proc, nullptr, TRUE)) return;
CONTEXT ctx = *faulting;
STACKFRAME64 sf;
std::memset(&sf, 0, sizeof sf);
DWORD machine = 0;
#if defined(_M_X64) || defined(__x86_64__)
machine = IMAGE_FILE_MACHINE_AMD64;
sf.AddrPC.Offset = ctx.Rip;
sf.AddrFrame.Offset = ctx.Rbp;
sf.AddrStack.Offset = ctx.Rsp;
#elif defined(_M_IX86) || defined(__i386__)
machine = IMAGE_FILE_MACHINE_I386;
sf.AddrPC.Offset = ctx.Eip;
sf.AddrFrame.Offset = ctx.Ebp;
sf.AddrStack.Offset = ctx.Esp;
#elif defined(_M_ARM64) || defined(__aarch64__)
machine = IMAGE_FILE_MACHINE_ARM64;
sf.AddrPC.Offset = ctx.Pc;
sf.AddrFrame.Offset = ctx.Fp;
sf.AddrStack.Offset = ctx.Sp;
#else
return;
#endif
sf.AddrPC.Mode = AddrModeFlat;
sf.AddrFrame.Mode = AddrModeFlat;
sf.AddrStack.Mode = AddrModeFlat;
alignas(SYMBOL_INFO) char symbuf[sizeof(SYMBOL_INFO) + 256];
char line[640];
for (int i = 0; i < 48; ++i) {
if (!::StackWalk64(machine, proc, thread, &sf, &ctx, nullptr, ::SymFunctionTableAccess64,
::SymGetModuleBase64, nullptr) ||
sf.AddrPC.Offset == 0) {
break;
}
const DWORD64 addr = sf.AddrPC.Offset;
SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(symbuf);
std::memset(sym, 0, sizeof(SYMBOL_INFO));
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
sym->MaxNameLen = 255;
DWORD64 disp = 0;
if (::SymFromAddr(proc, addr, &disp, sym)) {
std::snprintf(line, sizeof line, " %s+0x%llx [0x%llx]\n", sym->Name,
static_cast<unsigned long long>(disp), static_cast<unsigned long long>(addr));
} else {
HMODULE mod = nullptr;
char path[MAX_PATH] = "?";
if (::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(addr), &mod) &&
mod != nullptr) {
::GetModuleFileNameA(mod, path, MAX_PATH);
}
const char* base = std::strrchr(path, '\\');
std::snprintf(line, sizeof line, " %s+0x%llx\n", base ? base + 1 : path,
static_cast<unsigned long long>(addr - reinterpret_cast<DWORD64>(mod)));
}
out(line);
}
}
inline void finish() {
out("[antlrlab] This is a bug in antlrlab or in the ANTLR C++ runtime, not in your grammar.\n");
out("[antlrlab] Please report it with the stack trace above, the grammar and the input.\n");
::TerminateProcess(::GetCurrentProcess(), 2);
}
inline void header(const char* what) {
out("\n[antlrlab] fatal error: ");
out(what);
out("\n[antlrlab] while: ");
out(shared().phase[0] ? shared().phase : "(unknown)");
}
inline LONG WINAPI onException(EXCEPTION_POINTERS* ep) {
static std::atomic<int> entered{0};
if (entered.exchange(1) != 0) ::TerminateProcess(::GetCurrentProcess(), 2);
const EXCEPTION_RECORD* rec = ep->ExceptionRecord;
header(exceptionName(rec->ExceptionCode));
char line[96];
if (rec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && rec->NumberParameters >= 2) {
std::snprintf(line, sizeof line, "\n[antlrlab] address: 0x%llx",
static_cast<unsigned long long>(rec->ExceptionInformation[1]));
out(line);
} else if (rec->ExceptionCode != EXCEPTION_STACK_OVERFLOW) {
std::snprintf(line, sizeof line, "\n[antlrlab] exception code: 0x%08lx", static_cast<unsigned long>(rec->ExceptionCode));
out(line);
}
out("\n[antlrlab] stack trace (innermost first):\n");
printStackTrace(ep->ContextRecord);
finish();
return EXCEPTION_EXECUTE_HANDLER;
}
extern "C" inline void onAbort(int) {
header("abort (usually an uncaught exception or a failed assertion)");
out("\n");
finish();
}
} // namespace detail
inline void installHandlers() {
::SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
::SetUnhandledExceptionFilter(detail::onException);
std::signal(SIGABRT, detail::onAbort);
#ifdef _MSC_VER
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
#endif
}
// Keeps some stack in reserve on this thread so that the exception filter
// can still run after a stack overflow. One per thread that may overflow.
class AltStack {
public:
AltStack() {
ULONG bytes = 128 * 1024;
::SetThreadStackGuarantee(&bytes);
}
AltStack(const AltStack&) = delete;
AltStack& operator=(const AltStack&) = delete;
};
#else // some other system without signals: no crash reports
inline const char* signalName(int) { return "a fatal error"; }
inline void installHandlers() {}
class AltStack {};
#endif
} // namespace crash
} // namespace lab
+394
View File
@@ -0,0 +1,394 @@
// A minimal HTTP/1.1 server for the local web UI. It only listens on the
// loopback interface, answers one request per connection ("Connection:
// close"), and runs each connection on its own thread so that an idle
// browser pre-connection cannot block anything.
//
// POSIX sockets, with a Winsock branch for Windows.
#pragma once
#include "platform.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <functional>
#include <map>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "crash.hpp"
#ifndef _WIN32
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#endif
namespace lab {
struct Request {
std::string method;
std::string target; // raw request target
std::string path; // decoded path, without the query
std::map<std::string, std::string> query;
std::map<std::string, std::string> headers; // names lower-cased
std::string body;
std::string header(const std::string& name) const {
const auto it = headers.find(name);
return it == headers.end() ? std::string() : it->second;
}
};
struct Response {
int status = 200;
std::string contentType = "text/plain; charset=utf-8";
std::string body;
std::vector<std::pair<std::string, std::string>> headers;
};
using Handler = std::function<Response(const Request&)>;
namespace http {
constexpr size_t kMaxHeaderBytes = 64 * 1024;
constexpr size_t kMaxBodyBytes = 24 * 1024 * 1024;
#ifdef _WIN32
using Sock = SOCKET;
inline bool validSock(Sock s) { return s != INVALID_SOCKET; }
inline void closeSock(Sock s) { closesocket(s); }
constexpr Sock kBadSock = INVALID_SOCKET;
#else
using Sock = int;
inline bool validSock(Sock s) { return s >= 0; }
inline void closeSock(Sock s) { ::close(s); }
constexpr Sock kBadSock = -1;
#endif
inline int hexVal(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
inline std::string percentDecode(const std::string& s, bool plusIsSpace) {
std::string o;
o.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '%' && i + 2 < s.size() && hexVal(s[i + 1]) >= 0 && hexVal(s[i + 2]) >= 0) {
o += static_cast<char>(hexVal(s[i + 1]) * 16 + hexVal(s[i + 2]));
i += 2;
} else if (s[i] == '+' && plusIsSpace) {
o += ' ';
} else {
o += s[i];
}
}
return o;
}
inline std::string lower(std::string s) {
for (char& c : s) {
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
}
return s;
}
inline std::string trim(const std::string& s) {
size_t a = 0, b = s.size();
while (a < b && (s[a] == ' ' || s[a] == '\t')) ++a;
while (b > a && (s[b - 1] == ' ' || s[b - 1] == '\t' || s[b - 1] == '\r')) --b;
return s.substr(a, b - a);
}
inline const char* statusText(int code) {
switch (code) {
case 100: return "Continue";
case 200: return "OK";
case 204: return "No Content";
case 302: return "Found";
case 400: return "Bad Request";
case 403: return "Forbidden";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 408: return "Request Timeout";
case 409: return "Conflict";
case 411: return "Length Required";
case 413: return "Payload Too Large";
case 415: return "Unsupported Media Type";
case 431: return "Request Header Fields Too Large";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
default: return "OK";
}
}
inline bool sendAll(Sock s, const char* data, size_t n) {
size_t sent = 0;
while (sent < n) {
#ifdef _WIN32
const int r = ::send(s, data + sent, static_cast<int>(n - sent), 0);
#elif defined(MSG_NOSIGNAL)
const ssize_t r = ::send(s, data + sent, n - sent, MSG_NOSIGNAL);
#else
const ssize_t r = ::send(s, data + sent, n - sent, 0);
#endif
if (r <= 0) return false;
sent += static_cast<size_t>(r);
}
return true;
}
inline void writeResponse(Sock s, const Response& r) {
std::string head = "HTTP/1.1 " + std::to_string(r.status) + " " + statusText(r.status) + "\r\n";
head += "Content-Type: " + r.contentType + "\r\n";
head += "Content-Length: " + std::to_string(r.body.size()) + "\r\n";
head += "Connection: close\r\n";
head += "Cache-Control: no-store\r\n";
head += "X-Content-Type-Options: nosniff\r\n";
head += "Referrer-Policy: no-referrer\r\n";
head += "X-Frame-Options: DENY\r\n";
head += "Cross-Origin-Resource-Policy: same-origin\r\n";
for (const auto& h : r.headers) head += h.first + ": " + h.second + "\r\n";
head += "\r\n";
if (!sendAll(s, head.data(), head.size())) return;
if (!r.body.empty()) sendAll(s, r.body.data(), r.body.size());
}
inline Response errorResponse(int code, const std::string& msg) {
Response r;
r.status = code;
r.contentType = "application/json; charset=utf-8";
r.body = "{\"error\":\"" + msg + "\"}";
return r;
}
// Reads and parses one request. Returns 0 on success, otherwise the HTTP
// status to answer with (-1: just drop the connection).
inline int readRequest(Sock s, Request& rq) {
std::string buf;
char chunk[8192];
size_t headerEnd = std::string::npos;
while (headerEnd == std::string::npos) {
const int n = static_cast<int>(::recv(s, chunk, static_cast<int>(sizeof chunk), 0));
if (n <= 0) return buf.empty() ? -1 : 408;
buf.append(chunk, static_cast<size_t>(n));
headerEnd = buf.find("\r\n\r\n");
if (headerEnd == std::string::npos && buf.size() > kMaxHeaderBytes) return 431;
}
const std::string head = buf.substr(0, headerEnd);
std::string body = buf.substr(headerEnd + 4);
size_t eol = head.find("\r\n");
const std::string line = head.substr(0, eol);
const size_t sp1 = line.find(' ');
const size_t sp2 = line.rfind(' ');
if (sp1 == std::string::npos || sp2 == sp1) return 400;
rq.method = line.substr(0, sp1);
rq.target = line.substr(sp1 + 1, sp2 - sp1 - 1);
if (line.compare(sp2 + 1, 5, "HTTP/") != 0) return 400;
while (eol != std::string::npos) {
const size_t start = eol + 2;
eol = head.find("\r\n", start);
const std::string h = head.substr(start, eol == std::string::npos ? std::string::npos : eol - start);
const size_t colon = h.find(':');
if (colon == std::string::npos || colon == 0) continue;
rq.headers[lower(h.substr(0, colon))] = trim(h.substr(colon + 1));
}
const size_t qm = rq.target.find('?');
rq.path = percentDecode(rq.target.substr(0, qm), false);
if (qm != std::string::npos) {
const std::string qs = rq.target.substr(qm + 1);
size_t i = 0;
while (i <= qs.size()) {
const size_t amp = qs.find('&', i);
const std::string pair = qs.substr(i, amp == std::string::npos ? std::string::npos : amp - i);
if (!pair.empty()) {
const size_t eq = pair.find('=');
const std::string k = percentDecode(pair.substr(0, eq), true);
const std::string v = eq == std::string::npos ? "" : percentDecode(pair.substr(eq + 1), true);
rq.query[k] = v;
}
if (amp == std::string::npos) break;
i = amp + 1;
}
}
if (!rq.header("transfer-encoding").empty()) return 501;
size_t want = 0;
const std::string cl = rq.header("content-length");
if (!cl.empty()) {
for (char c : cl) {
if (c < '0' || c > '9' || cl.size() > 12) return 400;
}
want = static_cast<size_t>(std::stoull(cl));
if (want > kMaxBodyBytes) return 413;
} else if (rq.method == "POST" || rq.method == "PUT") {
return 411;
}
if (want > body.size() && lower(rq.header("expect")) == "100-continue") {
const char* c100 = "HTTP/1.1 100 Continue\r\n\r\n";
sendAll(s, c100, std::strlen(c100));
}
while (body.size() < want) {
const int n = static_cast<int>(::recv(s, chunk, static_cast<int>(sizeof chunk), 0));
if (n <= 0) return 408;
body.append(chunk, static_cast<size_t>(n));
}
body.resize(want);
rq.body = std::move(body);
return 0;
}
#ifdef _WIN32
struct WinsockInit {
WinsockInit() {
WSADATA d;
WSAStartup(MAKEWORD(2, 2), &d);
}
~WinsockInit() { WSACleanup(); }
};
#endif
} // namespace http
class HttpServer {
public:
HttpServer() {
#ifdef _WIN32
static http::WinsockInit init;
#else
::signal(SIGPIPE, SIG_IGN);
#endif
}
~HttpServer() {
if (http::validSock(listen_)) http::closeSock(listen_);
}
HttpServer(const HttpServer&) = delete;
HttpServer& operator=(const HttpServer&) = delete;
// Tries each port in turn (0 = let the system choose). Loopback only.
bool listenOn(const std::vector<int>& ports, std::string& error) {
for (int p : ports) {
http::Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
if (!http::validSock(s)) {
error = "cannot create a socket";
return false;
}
int yes = 1;
#ifdef _WIN32
// Without this, another program could bind the same port and share our traffic.
::setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&yes), sizeof yes);
::SetHandleInformation(reinterpret_cast<HANDLE>(s), HANDLE_FLAG_INHERIT, 0); // not for the Java child
#else
::setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof yes);
#endif
sockaddr_in addr;
std::memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(p));
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (::bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof addr) != 0 || ::listen(s, 32) != 0) {
http::closeSock(s);
continue;
}
sockaddr_in bound;
#ifdef _WIN32
int len = sizeof bound;
#else
socklen_t len = sizeof bound;
#endif
::getsockname(s, reinterpret_cast<sockaddr*>(&bound), &len);
port_ = ntohs(bound.sin_port);
listen_ = s;
return true;
}
error = ports.size() == 1 ? "port " + std::to_string(ports[0]) + " is not available"
: "no free port found";
return false;
}
int port() const { return port_; }
// Blocks until `stop` becomes true.
void serve(const Handler& handler, const std::atomic<bool>& stop) {
while (!stop.load()) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(listen_, &fds);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200 * 1000;
#ifdef _WIN32
const int ready = ::select(0, &fds, nullptr, nullptr, &tv);
#else
const int ready = ::select(listen_ + 1, &fds, nullptr, nullptr, &tv);
#endif
if (ready <= 0) continue;
http::Sock c = ::accept(listen_, nullptr, nullptr);
if (!http::validSock(c)) continue;
#ifdef _WIN32
::SetHandleInformation(reinterpret_cast<HANDLE>(c), HANDLE_FLAG_INHERIT, 0);
#endif
active_.fetch_add(1);
std::thread([this, c, &handler]() {
connection(c, handler);
active_.fetch_sub(1);
}).detach();
}
for (int i = 0; i < 100 && active_.load() > 0; ++i) { // let running requests finish
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
private:
http::Sock listen_ = http::kBadSock;
int port_ = 0;
std::atomic<int> active_{0};
static void connection(http::Sock c, const Handler& handler) {
crash::AltStack altStack;
#ifdef _WIN32
DWORD ms = 15000;
::setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&ms), sizeof ms);
::setsockopt(c, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&ms), sizeof ms);
#else
timeval tv;
tv.tv_sec = 15;
tv.tv_usec = 0;
::setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
::setsockopt(c, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
#endif
Request rq;
const int rc = http::readRequest(c, rq);
if (rc > 0) {
http::writeResponse(c, http::errorResponse(rc, http::statusText(rc)));
} else if (rc == 0) {
Response r;
try {
r = handler(rq);
} catch (const std::exception& e) {
r = http::errorResponse(500, "internal error");
(void)e;
}
http::writeResponse(c, r);
}
http::closeSock(c);
}
};
} // namespace lab
+107
View File
@@ -0,0 +1,107 @@
// Reads the ".interp" files that the ANTLR tool writes next to generated code.
//
// Format (one section per header, sections separated by a blank line):
//
// token literal names: one entry per token type, "null" if none
// token symbolic names: one entry per token type, "null" if none
// rule names:
// channel names: lexer only
// mode names: lexer only
// atn:
// [4, 1, 12, ...] serialized ATN, comma separated integers
//
// The runtime's LexerInterpreter / ParserInterpreter can run a grammar straight
// from this data, which is what makes a "grammar in, tree out" tool possible
// without generating and compiling C++ code for every grammar.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace lab {
struct InterpData {
std::string name; // file stem, e.g. "ExprLexer"
bool isLexer = false;
std::vector<std::string> literalNames; // "" where the file says null
std::vector<std::string> symbolicNames; // "" where the file says null
std::vector<std::string> ruleNames;
std::vector<std::string> channelNames;
std::vector<std::string> modeNames;
std::vector<int32_t> atn;
};
inline bool parseInterp(const std::string& text, InterpData& out, std::string& error) {
enum class Sec { None, Literal, Symbolic, Rules, Channels, Modes, Atn };
Sec sec = Sec::None;
bool sawAtn = false, sawChannels = false, sawModes = false;
std::string atnText;
size_t pos = 0;
while (pos <= text.size()) {
const size_t nl = text.find('\n', pos);
std::string line = text.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos);
pos = (nl == std::string::npos) ? text.size() + 1 : nl + 1;
if (!line.empty() && line.back() == '\r') line.pop_back();
if (sec == Sec::Atn) { // everything after "atn:" is the serialized ATN
atnText += line;
atnText += ' ';
continue;
}
if (line == "token literal names:") { sec = Sec::Literal; continue; }
if (line == "token symbolic names:") { sec = Sec::Symbolic; continue; }
if (line == "rule names:") { sec = Sec::Rules; continue; }
if (line == "channel names:") { sec = Sec::Channels; sawChannels = true; continue; }
if (line == "mode names:") { sec = Sec::Modes; sawModes = true; continue; }
if (line == "atn:") { sec = Sec::Atn; sawAtn = true; continue; }
if (line.empty()) { sec = Sec::None; continue; }
switch (sec) {
case Sec::Literal: out.literalNames.push_back(line == "null" ? "" : line); break;
case Sec::Symbolic: out.symbolicNames.push_back(line == "null" ? "" : line); break;
case Sec::Rules: out.ruleNames.push_back(line); break;
case Sec::Channels: out.channelNames.push_back(line); break;
case Sec::Modes: out.modeNames.push_back(line); break;
default: break;
}
}
if (!sawAtn) {
error = "no \"atn:\" section (is this really an ANTLR .interp file?)";
return false;
}
// The serialized ATN is "[4, 1, 12, ...]": pull out every integer.
out.atn.clear();
for (size_t i = 0; i < atnText.size();) {
const char c = atnText[i];
const bool digit = c >= '0' && c <= '9';
if (!digit && !(c == '-' && i + 1 < atnText.size() && atnText[i + 1] >= '0' && atnText[i + 1] <= '9')) {
++i;
continue;
}
const bool neg = (c == '-');
if (neg) ++i;
long long v = 0;
while (i < atnText.size() && atnText[i] >= '0' && atnText[i] <= '9') {
v = v * 10 + (atnText[i] - '0');
++i;
}
out.atn.push_back(static_cast<int32_t>(neg ? -v : v));
}
if (out.atn.empty()) {
error = "the \"atn:\" section is empty";
return false;
}
if (out.atn.front() != 4) {
error = "serialized ATN has version " + std::to_string(out.atn.front()) +
" but this program reads version 4 (written by ANTLR 4.10 and newer)";
return false;
}
out.isLexer = sawChannels || sawModes;
return true;
}
} // namespace lab
+329
View File
@@ -0,0 +1,329 @@
// Runs the part of the program that talks to the ANTLR runtime somewhere it
// cannot take everything else down:
//
// * on a thread with a very large stack, because the runtime's prediction
// code and our own tree code recurse deeply on real grammars (default
// stacks are 8 MB on the main thread and only 512 KB on macOS threads);
// * in a forked child process, so a crash (or an endless run) becomes an
// error message and the parent - the web server, the watch loop - lives on;
// * with a time limit.
//
// Windows has no fork: there the work runs in-process, on a thread with a
// large stack. A crash there still gets a report (what the program was doing
// and a stack trace, see crash.hpp) but ends the program, and --timeout is
// not enforced.
#pragma once
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <functional>
#include <stdexcept>
#include <string>
#include "crash.hpp"
#ifdef LAB_POSIX
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <sys/resource.h>
#include <sys/wait.h>
#elif defined(_WIN32)
#include <process.h>
#endif
namespace lab {
// Whether the runtime can be run in a child process that may crash or be
// stopped (POSIX), or only in this process (Windows).
#ifdef LAB_POSIX
constexpr bool kHasIsolation = true;
#else
constexpr bool kHasIsolation = false;
#endif
struct Outcome {
enum class Kind { Ok, Crashed, TimedOut, Cancelled, Failed };
Kind kind = Kind::Failed;
std::string payload; // what the work function returned
int exitCode = 0; // what the work function reported through its argument
int signal = 0; // when Crashed
std::string phase; // what the child was doing at the end
std::string message; // for Failed: the reason
};
// The work function returns a payload string and may set an exit code.
using Work = std::function<std::string(int& exitCode)>;
// ------------------------------------------------------------ big stack ----
#ifdef LAB_POSIX
namespace detail {
struct StackJob {
const std::function<void()>* fn;
std::exception_ptr error;
};
inline void* stackTrampoline(void* p) {
StackJob* job = static_cast<StackJob*>(p);
crash::AltStack alt;
try {
(*job->fn)();
} catch (...) {
job->error = std::current_exception();
}
return nullptr;
}
} // namespace detail
// Runs fn on a new thread with a big stack and waits for it. Falls back to a
// smaller stack, and finally to the calling thread, if the system refuses.
inline void runOnBigStack(const std::function<void()>& fn) {
detail::StackJob job{&fn, nullptr};
const size_t MB = 1024 * 1024;
for (size_t mb : {size_t(512), size_t(128), size_t(32)}) {
pthread_attr_t attr;
if (pthread_attr_init(&attr) != 0) break;
if (pthread_attr_setstacksize(&attr, mb * MB) != 0) {
pthread_attr_destroy(&attr);
continue;
}
pthread_t th;
const int rc = pthread_create(&th, &attr, detail::stackTrampoline, &job);
pthread_attr_destroy(&attr);
if (rc != 0) continue;
pthread_join(th, nullptr);
if (job.error) std::rethrow_exception(job.error);
return;
}
fn();
}
#elif defined(_WIN32)
namespace detail {
struct StackJob {
const std::function<void()>* fn;
std::exception_ptr error;
bool finished = false; // still false after the wait: the thread was killed (stack overflow)
};
inline unsigned __stdcall stackTrampoline(void* p) {
StackJob* job = static_cast<StackJob*>(p);
crash::AltStack alt;
try {
(*job->fn)();
} catch (...) {
job->error = std::current_exception();
}
job->finished = true;
return 0;
}
} // namespace detail
// Runs fn on a new thread with a big stack and waits for it. Falls back to a
// smaller stack, and finally to the calling thread, if the system refuses.
inline void runOnBigStack(const std::function<void()>& fn) {
detail::StackJob job{&fn, nullptr, false};
const size_t MB = 1024 * 1024;
for (size_t mb : {size_t(512), size_t(128), size_t(32)}) {
if (sizeof(void*) < 8 && mb > 128) continue; // 32-bit: address space is scarce
unsigned id = 0;
const uintptr_t h = ::_beginthreadex(nullptr, static_cast<unsigned>(mb * MB), detail::stackTrampoline, &job,
STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
if (h == 0) continue;
HANDLE th = reinterpret_cast<HANDLE>(h);
::WaitForSingleObject(th, INFINITE);
::CloseHandle(th);
if (job.error) std::rethrow_exception(job.error);
if (!job.finished) throw std::runtime_error("the worker thread ended abnormally (stack overflow?)");
return;
}
fn();
}
#else
inline void runOnBigStack(const std::function<void()>& fn) { fn(); }
#endif
// ------------------------------------------------------------- isolation ----
namespace detail {
inline Outcome runInline(const Work& fn) {
Outcome o;
try {
runOnBigStack([&] { o.payload = fn(o.exitCode); });
o.kind = Outcome::Kind::Ok;
} catch (const std::exception& e) {
o.kind = Outcome::Kind::Failed;
o.message = e.what();
} catch (...) {
o.kind = Outcome::Kind::Failed;
o.message = "unknown exception";
}
return o;
}
} // namespace detail
// One-line description of a failed Outcome, for people.
inline std::string describe(const Outcome& o) {
const std::string where = o.phase.empty() ? std::string() : " while " + o.phase;
switch (o.kind) {
case Outcome::Kind::Crashed:
return std::string("The ANTLR C++ runtime crashed with ") + crash::signalName(o.signal) + where +
". antlrlab itself is still running. This is a bug in antlrlab or in the runtime rather than in your "
"grammar; the terminal that started antlrlab has a stack trace.";
case Outcome::Kind::TimedOut:
return "Stopped after " + o.message + where +
". The grammar or input may be extremely ambiguous. Raise the limit with --timeout.";
case Outcome::Kind::Cancelled:
return "Cancelled.";
case Outcome::Kind::Failed:
return "The run failed" + where + ": " + (o.message.empty() ? "unknown error" : o.message);
default:
return "";
}
}
// Runs fn in a forked child (POSIX) and returns what it produced, or how it
// died. timeoutSeconds <= 0 means no limit. isolate = false runs in-process.
inline Outcome runIsolated(const Work& fn, int timeoutSeconds, bool isolate = true) {
#ifdef LAB_POSIX
if (!isolate) return detail::runInline(fn);
int fds[2];
if (::pipe(fds) != 0) return detail::runInline(fn);
std::fflush(nullptr);
crash::setPhase("starting");
const pid_t pid = ::fork();
if (pid < 0) {
::close(fds[0]);
::close(fds[1]);
return detail::runInline(fn);
}
if (pid == 0) { // ---- child
::close(fds[0]);
::signal(SIGINT, SIG_DFL);
::signal(SIGTERM, SIG_DFL);
struct rlimit nocore;
nocore.rlim_cur = nocore.rlim_max = 0;
::setrlimit(RLIMIT_CORE, &nocore);
int code = 0;
char tag = 'R';
std::string body;
try {
runOnBigStack([&] { body = fn(code); });
} catch (const std::exception& e) {
tag = 'E';
body = e.what();
} catch (...) {
tag = 'E';
body = "unknown exception";
}
std::fflush(nullptr);
std::string frame;
frame.reserve(body.size() + 5);
frame.push_back(tag);
for (int i = 0; i < 4; ++i) frame.push_back(static_cast<char>((code >> (8 * i)) & 0xFF));
frame += body;
size_t off = 0;
while (off < frame.size()) {
const ssize_t w = ::write(fds[1], frame.data() + off, frame.size() - off);
if (w < 0 && errno == EINTR) continue;
if (w <= 0) break;
off += static_cast<size_t>(w);
}
::close(fds[1]);
::_exit(0);
}
// ---- parent
::close(fds[1]);
std::string data;
bool timedOut = false;
const auto start = std::chrono::steady_clock::now();
for (;;) {
int waitMs = 500;
if (timeoutSeconds > 0) {
const long long elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
const long long left = static_cast<long long>(timeoutSeconds) * 1000 - elapsed;
if (left <= 0) {
timedOut = true;
break;
}
if (left < waitMs) waitMs = static_cast<int>(left);
}
struct pollfd pfd;
pfd.fd = fds[0];
pfd.events = POLLIN;
pfd.revents = 0;
const int rc = ::poll(&pfd, 1, waitMs);
if (rc < 0) {
if (errno == EINTR) continue;
break;
}
if (rc == 0) continue;
char buf[65536];
const ssize_t n = ::read(fds[0], buf, sizeof buf);
if (n > 0) {
data.append(buf, static_cast<size_t>(n));
} else if (n == 0) {
break;
} else if (errno != EINTR) {
break;
}
}
::close(fds[0]);
if (timedOut) ::kill(pid, SIGKILL);
int status = 0;
while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {
}
Outcome o;
o.phase = crash::phase();
if (timedOut) {
o.kind = Outcome::Kind::TimedOut;
o.message = std::to_string(timeoutSeconds) + " seconds";
return o;
}
if (WIFSIGNALED(status)) {
o.signal = WTERMSIG(status);
o.kind = (o.signal == SIGINT || o.signal == SIGTERM) ? Outcome::Kind::Cancelled : Outcome::Kind::Crashed;
return o;
}
if (WIFEXITED(status) && WEXITSTATUS(status) == 0 && data.size() >= 5) {
int code = 0;
for (int i = 0; i < 4; ++i) code |= static_cast<int>(static_cast<unsigned char>(data[1 + static_cast<size_t>(i)])) << (8 * i);
o.exitCode = code;
o.payload = data.substr(5);
if (data[0] == 'R') {
o.kind = Outcome::Kind::Ok;
} else {
o.kind = Outcome::Kind::Failed;
o.message = o.payload;
o.payload.clear();
}
return o;
}
o.kind = Outcome::Kind::Failed;
o.message = "the worker process ended unexpectedly (exit status " +
std::to_string(WIFEXITED(status) ? WEXITSTATUS(status) : -1) + ")";
return o;
#else
(void)timeoutSeconds;
(void)isolate;
return detail::runInline(fn);
#endif
}
} // namespace lab
+277
View File
@@ -0,0 +1,277 @@
// A small, strict JSON parser. It only has to read the request bodies that the
// web UI sends, but it validates everything (depth limit, escapes, UTF-16
// surrogates) because those bodies arrive over a socket.
#pragma once
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace lab {
struct Json {
enum class Type { Null, Bool, Number, String, Array, Object };
Type type = Type::Null;
bool b = false;
double n = 0;
std::string s;
std::vector<Json> a;
std::vector<std::pair<std::string, Json>> o;
const Json* find(const std::string& key) const {
if (type != Type::Object) return nullptr;
for (const auto& kv : o) {
if (kv.first == key) return &kv.second;
}
return nullptr;
}
std::string str(const std::string& key, const std::string& def = "") const {
const Json* v = find(key);
return (v && v->type == Type::String) ? v->s : def;
}
bool boolean(const std::string& key, bool def = false) const {
const Json* v = find(key);
return (v && v->type == Type::Bool) ? v->b : def;
}
double number(const std::string& key, double def = 0) const {
const Json* v = find(key);
return (v && v->type == Type::Number) ? v->n : def;
}
const std::vector<Json>* array(const std::string& key) const {
const Json* v = find(key);
return (v && v->type == Type::Array) ? &v->a : nullptr;
}
};
namespace detail {
class JsonParser {
public:
explicit JsonParser(const std::string& text) : t_(text) {}
bool parse(Json& out, std::string& error) {
skipWs();
if (!value(out, 0)) {
error = err_.empty() ? "invalid JSON" : err_;
return false;
}
skipWs();
if (pos_ != t_.size()) {
error = "unexpected text after the JSON value";
return false;
}
return true;
}
private:
const std::string& t_;
size_t pos_ = 0;
std::string err_;
bool fail(const char* msg) {
if (err_.empty()) err_ = std::string(msg) + " at offset " + std::to_string(pos_);
return false;
}
void skipWs() {
while (pos_ < t_.size() && (t_[pos_] == ' ' || t_[pos_] == '\t' || t_[pos_] == '\n' || t_[pos_] == '\r')) ++pos_;
}
bool literal(const char* word) {
size_t i = 0;
while (word[i]) {
if (pos_ + i >= t_.size() || t_[pos_ + i] != word[i]) return false;
++i;
}
pos_ += i;
return true;
}
static void appendUtf8(std::string& out, uint32_t cp) {
if (cp < 0x80) {
out += static_cast<char>(cp);
} else if (cp < 0x800) {
out += static_cast<char>(0xC0 | (cp >> 6));
out += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
out += static_cast<char>(0xE0 | (cp >> 12));
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out += static_cast<char>(0x80 | (cp & 0x3F));
} else {
out += static_cast<char>(0xF0 | (cp >> 18));
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out += static_cast<char>(0x80 | (cp & 0x3F));
}
}
bool hex4(uint32_t& v) {
if (pos_ + 4 > t_.size()) return fail("truncated \\u escape");
v = 0;
for (int i = 0; i < 4; ++i) {
const char c = t_[pos_++];
v <<= 4;
if (c >= '0' && c <= '9') v |= static_cast<uint32_t>(c - '0');
else if (c >= 'a' && c <= 'f') v |= static_cast<uint32_t>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') v |= static_cast<uint32_t>(c - 'A' + 10);
else return fail("bad \\u escape");
}
return true;
}
bool string(std::string& out) {
if (pos_ >= t_.size() || t_[pos_] != '"') return fail("expected a string");
++pos_;
out.clear();
while (pos_ < t_.size()) {
const unsigned char c = static_cast<unsigned char>(t_[pos_++]);
if (c == '"') return true;
if (c < 0x20) return fail("control character in string");
if (c != '\\') {
out += static_cast<char>(c);
continue;
}
if (pos_ >= t_.size()) break;
const char e = t_[pos_++];
switch (e) {
case '"': out += '"'; break;
case '\\': out += '\\'; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'n': out += '\n'; break;
case 'r': out += '\r'; break;
case 't': out += '\t'; break;
case 'u': {
uint32_t cp = 0;
if (!hex4(cp)) return false;
if (cp >= 0xD800 && cp <= 0xDBFF) {
uint32_t lo = 0;
if (pos_ + 1 < t_.size() && t_[pos_] == '\\' && t_[pos_ + 1] == 'u') {
pos_ += 2;
if (!hex4(lo)) return false;
}
if (lo >= 0xDC00 && lo <= 0xDFFF) cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
else cp = 0xFFFD;
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
cp = 0xFFFD;
}
appendUtf8(out, cp);
break;
}
default: return fail("bad escape");
}
}
return fail("unterminated string");
}
bool number(double& out) {
const size_t start = pos_;
if (pos_ < t_.size() && t_[pos_] == '-') ++pos_;
bool digits = false;
while (pos_ < t_.size() && ((t_[pos_] >= '0' && t_[pos_] <= '9') || t_[pos_] == '.' || t_[pos_] == 'e' ||
t_[pos_] == 'E' || t_[pos_] == '+' || t_[pos_] == '-')) {
if (t_[pos_] >= '0' && t_[pos_] <= '9') digits = true;
++pos_;
}
if (!digits) return fail("bad number");
try {
out = std::stod(t_.substr(start, pos_ - start));
} catch (...) {
return fail("bad number");
}
return true;
}
bool value(Json& out, int depth) {
if (depth > 32) return fail("nesting too deep");
if (pos_ >= t_.size()) return fail("unexpected end");
const char c = t_[pos_];
if (c == '{') {
++pos_;
out.type = Json::Type::Object;
skipWs();
if (pos_ < t_.size() && t_[pos_] == '}') {
++pos_;
return true;
}
for (;;) {
skipWs();
std::string key;
if (!string(key)) return false;
skipWs();
if (pos_ >= t_.size() || t_[pos_] != ':') return fail("expected ':'");
++pos_;
skipWs();
Json v;
if (!value(v, depth + 1)) return false;
out.o.emplace_back(std::move(key), std::move(v));
skipWs();
if (pos_ < t_.size() && t_[pos_] == ',') {
++pos_;
continue;
}
if (pos_ < t_.size() && t_[pos_] == '}') {
++pos_;
return true;
}
return fail("expected ',' or '}'");
}
}
if (c == '[') {
++pos_;
out.type = Json::Type::Array;
skipWs();
if (pos_ < t_.size() && t_[pos_] == ']') {
++pos_;
return true;
}
for (;;) {
skipWs();
Json v;
if (!value(v, depth + 1)) return false;
out.a.push_back(std::move(v));
skipWs();
if (pos_ < t_.size() && t_[pos_] == ',') {
++pos_;
continue;
}
if (pos_ < t_.size() && t_[pos_] == ']') {
++pos_;
return true;
}
return fail("expected ',' or ']'");
}
}
if (c == '"') {
out.type = Json::Type::String;
return string(out.s);
}
if (c == 't') {
out.type = Json::Type::Bool;
out.b = true;
return literal("true") || fail("bad literal");
}
if (c == 'f') {
out.type = Json::Type::Bool;
out.b = false;
return literal("false") || fail("bad literal");
}
if (c == 'n') {
out.type = Json::Type::Null;
return literal("null") || fail("bad literal");
}
out.type = Json::Type::Number;
return number(out.n);
}
};
} // namespace detail
inline bool parseJson(const std::string& text, Json& out, std::string& error) {
out = Json();
detail::JsonParser p(text);
return p.parse(out, error);
}
} // namespace lab
+775
View File
@@ -0,0 +1,775 @@
// antlrlab - a command line "ANTLR Lab": give it grammar files and an input,
// get a parse tree diagram, tree text, tokens and syntax validation.
//
// antlrlab Expr.g4 input.txt
// antlrlab -g CalcLexer.g4 -g CalcParser.g4 -i input.txt --watch --open
//
// Exit codes: 0 input is valid, 1 syntax errors, 2 grammar / tool / usage error.
#include "util.hpp" // first of ours: it includes platform.hpp (windows.h, <cstdio>) before the ANTLR runtime
#include <chrono>
#include <cstdio>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <thread>
#include <vector>
#include "analysis.hpp"
#include "antlr_tool.hpp"
#include "crash.hpp"
#include "isolate.hpp"
#include "pipeline.hpp"
#include "render.hpp"
#include "report.hpp"
#include "serve.hpp"
#include "types.hpp"
#ifndef LAB_VERSION
#define LAB_VERSION "1.0.0"
#endif
using namespace lab;
namespace {
// ---------------------------------------------------------------- options ----
struct Options {
std::vector<std::string> grammars;
std::vector<std::string> inputs; // plain arguments and -i; the CLI takes at most one
std::string inputFile;
std::string inputText;
bool hasText = false;
std::string startRule;
std::string outDir = "antlr-lab-out";
std::string libDir;
std::string jar;
bool watch = false;
bool open = false;
bool quiet = false;
bool tokens = false;
bool color = true;
bool serve = false;
int port = 0;
std::string root;
int timeout = 120; // seconds a run may take; 0 = no limit
bool selftest = false;
bool isolate = true; // run the ANTLR runtime in a child process
};
const char* kUsage =
"antlrlab " LAB_VERSION " - parse tree diagram, tokens and validation for ANTLR grammars\n"
"\n"
"Usage:\n"
" antlrlab [options] GRAMMAR.g4 [GRAMMAR2.g4] INPUT\n"
" antlrlab -g Lexer.g4 -g Parser.g4 -i input.txt\n"
" antlrlab --serve [FILES...] web UI: edit and run files on disk\n"
"\n"
"Grammar: one combined grammar, or a lexer grammar plus the parser grammar that\n"
"uses it. Files ending in .g4 are grammars; any other file is the input.\n"
"\n"
"Options:\n"
" -g, --grammar FILE grammar file (repeatable)\n"
" -i, --input FILE input file, or - for standard input\n"
" -t, --text TEXT input given directly on the command line\n"
" -s, --start RULE start rule (default: the first parser rule)\n"
" -o, --out DIR output directory (default: antlr-lab-out)\n"
" -l, --lib DIR directory with imported grammars / .tokens files\n"
" --antlr-jar FILE ANTLR tool jar (default: search, see README)\n"
" -w, --watch re-run whenever a grammar or the input file changes;\n"
" an open report.html refreshes itself\n"
" --open open report.html in the default browser\n"
" --tokens also print the token list\n"
" --serve start the web UI (editor, diagram, output) on 127.0.0.1\n"
" -p, --port N port for --serve (default: first free from 8765)\n"
" --root DIR directory the web UI may read and write (default: current)\n"
" -q, --quiet only print diagnostics and the verdict\n"
" --timeout SECS stop a run that takes longer (default 120, 0 = no limit)\n"
" --no-isolate run the ANTLR runtime inside this process (for debuggers)\n"
" --selftest check the installation with a built-in grammar, then exit\n"
" --no-color no ANSI colors (also honours NO_COLOR)\n"
" -h, --help show this help\n"
" -v, --version show the version\n"
"\n"
"Output files (in the output directory):\n"
" report.html tree.svg tree.txt tokens.txt problems.txt generated/\n"
"\n"
"Exit status: 0 input is valid, 1 syntax errors, 2 grammar, tool or usage error.\n";
bool endsWith(const std::string& s, const std::string& suffix) {
return s.size() >= suffix.size() && lowerCase(s.substr(s.size() - suffix.size())) == suffix;
}
// Returns -1 to carry on, otherwise the exit code.
int parseArgs(const std::vector<std::string>& args, Options& o) {
const int argc = static_cast<int>(args.size());
auto fail = [](const std::string& msg) {
std::fprintf(stderr, "antlrlab: %s\nTry 'antlrlab --help'.\n", msg.c_str());
return 2;
};
bool onlyPositional = false;
for (int i = 1; i < argc; ++i) {
std::string a = args[static_cast<size_t>(i)];
std::string inlineValue;
bool hasInline = false;
if (!onlyPositional && a.size() > 2 && a[0] == '-' && a[1] == '-') {
const size_t eq = a.find('=');
if (eq != std::string::npos) {
inlineValue = a.substr(eq + 1);
a = a.substr(0, eq);
hasInline = true;
}
}
auto value = [&](std::string& dst) -> bool {
if (hasInline) {
dst = inlineValue;
return true;
}
if (i + 1 >= argc) return false;
dst = args[static_cast<size_t>(++i)];
return true;
};
std::string v;
if (onlyPositional || a.empty() || a[0] != '-' || a == "-") {
if (endsWith(a, ".g4")) o.grammars.push_back(a);
else o.inputs.push_back(a);
} else if (a == "--") {
onlyPositional = true;
} else if (a == "-h" || a == "--help") {
std::fputs(kUsage, stdout);
return 0;
} else if (a == "-v" || a == "--version") {
std::printf("antlrlab %s (built for ANTLR %s)\n", LAB_VERSION, LAB_ANTLR_VERSION);
return 0;
} else if (a == "-g" || a == "--grammar") {
if (!value(v)) return fail(a + " needs a file name");
o.grammars.push_back(v);
} else if (a == "-i" || a == "--input") {
if (!value(v)) return fail(a + " needs a file name");
o.inputs.push_back(v);
} else if (a == "-t" || a == "--text") {
if (!value(v)) return fail(a + " needs some text");
if (o.hasText) return fail("more than one input given");
o.inputText = v;
o.hasText = true;
} else if (a == "-s" || a == "--start") {
if (!value(o.startRule)) return fail(a + " needs a rule name");
} else if (a == "-o" || a == "--out") {
if (!value(o.outDir)) return fail(a + " needs a directory");
} else if (a == "-l" || a == "--lib") {
if (!value(o.libDir)) return fail(a + " needs a directory");
} else if (a == "--antlr-jar") {
if (!value(o.jar)) return fail(a + " needs a file name");
} else if (a == "-w" || a == "--watch") {
o.watch = true;
} else if (a == "--open") {
o.open = true;
} else if (a == "--tokens") {
o.tokens = true;
} else if (a == "--serve") {
o.serve = true;
} else if (a == "-p" || a == "--port") {
if (!value(v)) return fail(a + " needs a port number");
char* end = nullptr;
const long n = std::strtol(v.c_str(), &end, 10);
if (v.empty() || *end != '\0' || n < 1 || n > 65535) return fail("invalid port: " + v);
o.port = static_cast<int>(n);
} else if (a == "--root") {
if (!value(o.root)) return fail(a + " needs a directory");
} else if (a == "-q" || a == "--quiet") {
o.quiet = true;
} else if (a == "--no-color") {
o.color = false;
} else if (a == "--no-isolate") {
o.isolate = false;
} else if (a == "--selftest") {
o.selftest = true;
} else if (a == "--timeout") {
if (!value(v)) return fail(a + " needs a number of seconds");
char* end = nullptr;
const long n = std::strtol(v.c_str(), &end, 10);
if (v.empty() || *end != '\0' || n < 0 || n > 86400) return fail("invalid timeout: " + v);
o.timeout = static_cast<int>(n);
} else {
return fail("unknown option " + a);
}
}
if (o.selftest) return -1;
if (o.serve) {
if (o.watch || o.hasText) return fail("--serve cannot be combined with --watch or -t");
return -1;
}
if (o.grammars.empty()) return fail("no grammar given. Pass at least one .g4 file.");
if (o.inputs.size() + (o.hasText ? 1 : 0) > 1) return fail("more than one input given");
if (!o.inputs.empty()) o.inputFile = o.inputs[0];
if (o.inputFile.empty() && !o.hasText) {
return fail("no input given. Pass an input file, or use -i FILE or -t TEXT.");
}
if (o.watch && o.inputFile == "-") return fail("--watch cannot be combined with reading standard input");
if (o.watch && o.hasText) return fail("--watch needs an input file, not -t");
return -1;
}
// ---------------------------------------------------------------- console ----
struct Console {
bool out = false; // colors on stdout
bool err = false; // colors on stderr
static std::string paint(bool on, const char* code, const std::string& s) {
return on ? std::string("\x1b[") + code + "m" + s + "\x1b[0m" : s;
}
std::string eBold(const std::string& s) const { return paint(err, "1", s); }
std::string eRed(const std::string& s) const { return paint(err, "1;31", s); }
std::string eYellow(const std::string& s) const { return paint(err, "1;33", s); }
std::string eGreen(const std::string& s) const { return paint(err, "1;32", s); }
std::string eDim(const std::string& s) const { return paint(err, "2", s); }
};
std::vector<std::string> splitLines(const std::string& text) {
std::vector<std::string> lines(1);
for (char c : text) {
if (c == '\n') lines.emplace_back();
else lines.back() += c;
}
for (auto& l : lines) {
if (!l.empty() && l.back() == '\r') l.pop_back();
}
return lines;
}
// Splits one line of UTF-8 into code points.
std::vector<std::string> codePoints(const std::string& line) {
std::vector<std::string> cps;
for (size_t i = 0; i < line.size();) {
size_t len = 1;
const unsigned char c = static_cast<unsigned char>(line[i]);
if (c >= 0xF0) len = 4;
else if (c >= 0xE0) len = 3;
else if (c >= 0xC0) len = 2;
cps.push_back(line.substr(i, len));
i += len;
}
return cps;
}
void printDiagnostic(const Console& con, const Diagnostic& d, const std::string& name,
const std::vector<std::string>& lines) {
const bool isErr = d.severity == Diagnostic::Severity::Error;
const std::string kind = isErr ? con.eRed("error") : con.eYellow("warning");
std::string where = name;
if (d.line > 0) where += ":" + std::to_string(d.line) + ":" + std::to_string(d.col);
std::fprintf(stderr, "%s: %s: %s\n", con.eBold(where).c_str(), kind.c_str(), d.message.c_str());
if (d.line == 0 || d.line > lines.size()) return;
const std::vector<std::string> cps = codePoints(lines[d.line - 1]);
std::string shown = lines[d.line - 1];
std::string pad;
for (size_t i = 0; i < d.col && i < cps.size(); ++i) pad += (cps[i] == "\t") ? "\t" : " ";
if (d.col > cps.size()) pad += std::string(d.col - cps.size(), ' ');
size_t len = 1;
if (d.start >= 0 && d.stop >= d.start) len = static_cast<size_t>(d.stop - d.start + 1);
const size_t room = d.col < cps.size() ? cps.size() - d.col : 1;
len = std::max<size_t>(1, std::min(len, room));
std::string marks = "^";
if (len > 1) marks += std::string(len - 1, '~');
std::fprintf(stderr, " %s\n %s%s\n", shown.c_str(), pad.c_str(),
(isErr ? con.eRed(marks) : con.eYellow(marks)).c_str());
}
std::string tokenLine(const TokenInfo& t, int64_t total) {
const int64_t s = t.isEof ? total : t.start;
const int64_t e = t.isEof ? total - 1 : t.stop;
std::string out = "[@" + std::to_string(t.index) + "," + std::to_string(s) + ":" + std::to_string(e) + "='" +
(t.isEof ? std::string("<EOF>") : visible(t.text)) + "',<" + t.typeName + ">,";
if (t.channel != "DEFAULT_TOKEN_CHANNEL" && t.channel != "0") out += "channel=" + t.channel + ",";
out += std::to_string(t.line) + ":" + std::to_string(t.col) + "]";
return out;
}
// ---------------------------------------------------------------- session ----
struct Session {
Options opt;
Tool tool;
fs::path outDir;
fs::path genDir;
// grammar stage
bool grammarOk = false;
std::string grammarError;
std::string toolLog;
std::vector<GrammarSource> sources;
ModelSpec spec; // what the .interp files say; the runtime objects are made in the worker
int version = 0;
bool firstRun = true;
std::string stdinCache;
bool stdinRead = false;
};
void removeStaleOutputs(const Session& s) {
std::error_code ec;
for (const char* n : {"tree.svg", "tree.txt", "tokens.txt", "problems.txt"}) fs::remove(s.outDir / n, ec);
}
// Runs the ANTLR tool and reads the .interp files it wrote.
void compileGrammar(Session& s) {
s.grammarOk = false;
s.grammarError.clear();
s.toolLog.clear();
s.sources.clear();
s.spec = ModelSpec();
std::vector<fs::path> paths;
for (const std::string& g : s.opt.grammars) {
std::string text;
if (!readFile(g, text)) {
s.grammarError = "cannot read grammar file: " + g;
return;
}
s.sources.push_back({pathToUtf8(pathFromUtf8(g).filename()), sanitizeUtf8(text)});
paths.push_back(pathFromUtf8(g));
}
const CompileResult r = compileGrammars(s.tool, paths, s.opt.libDir, s.genDir, s.outDir / "antlr-tool.log");
s.toolLog = r.log;
if (!r.ok) {
s.grammarError = r.summary;
return;
}
std::string err;
if (!loadInterps(r.interps, s.spec, err)) {
s.grammarError = err;
s.spec = ModelSpec();
return;
}
s.grammarOk = true;
}
bool readInput(Session& s, std::string& text, std::string& name, std::string& error) {
if (s.opt.hasText) {
text = s.opt.inputText;
name = "<text>";
return true;
}
if (s.opt.inputFile == "-") {
if (!s.stdinRead) {
std::ostringstream ss;
ss << std::cin.rdbuf();
s.stdinCache = ss.str();
s.stdinRead = true;
}
text = s.stdinCache;
name = "<stdin>";
return true;
}
if (!readFile(s.opt.inputFile, text)) {
error = "cannot read input file: " + s.opt.inputFile;
return false;
}
name = s.opt.inputFile;
return true;
}
bool writeOut(const fs::path& p, const std::string& data) {
if (writeFileAtomic(p, data)) return true;
std::fprintf(stderr, "antlrlab: cannot write %s\n", pathToUtf8(p).c_str());
return false;
}
void writeReport(Session& s, ReportInput& rin) {
rin.version = s.version;
rin.live = s.opt.watch;
rin.grammars = s.sources;
rin.toolLog = s.toolLog;
writeOut(s.outDir / "report.html", reportHtml(rin));
if (s.opt.watch) writeOut(s.outDir / "report-data.js", reportDataJs(rin));
}
// Everything that touches the ANTLR runtime: build it from the grammar data,
// run the input, write every output file, print the summary. This runs in an
// isolated worker (see runOnce), so a crash in here cannot take the caller down.
int analyzeAndWrite(Session& s, const Console& con, const std::string& text, const std::string& name,
const ReportInput& base) {
std::error_code ec;
ReportInput rin = base;
Model model;
std::string err;
if (!buildModel(s.spec, model, err)) {
rin.statusKind = "fail";
rin.statusText = "Grammar has errors";
rin.statusDetail = err;
rin.noDiagram = "The grammar could not be loaded, so there is nothing to draw.";
writeReport(s, rin);
removeStaleOutputs(s);
std::fflush(stdout);
std::fprintf(stderr, "%s %s\n", con.eRed("Grammar has errors:").c_str(), err.c_str());
return 2;
}
// ---- run the grammar
AnalysisResult res;
std::string runErr;
if (!analyze(model, text, s.opt.startRule, res, runErr)) {
rin.statusKind = "fail";
rin.statusText = "Cannot run the grammar";
rin.statusDetail = runErr;
rin.noDiagram = runErr;
writeReport(s, rin);
removeStaleOutputs(s);
std::fprintf(stderr, "antlrlab: %s\n", runErr.c_str());
return 2;
}
const size_t errors = res.errorCount();
const size_t warnings = res.warningCount();
const size_t realTokens = countRealTokens(res);
fillReportStatus(rin, res, model.hasParser);
// ---- files
crash::setPhase("writing the output files");
const int64_t total = static_cast<int64_t>(cpCount(text));
std::string tokText;
for (const TokenInfo& t : res.tokens) tokText += tokenLine(t, total) + "\n";
writeOut(s.outDir / "tokens.txt", tokText);
std::string problems;
const std::vector<std::string> lines = splitLines(text);
for (const Diagnostic& d : res.diagnostics) {
problems += name + ":" + std::to_string(d.line) + ":" + std::to_string(d.col) + ": " +
(d.severity == Diagnostic::Severity::Error ? "error" : "warning") + ": " + d.message + "\n";
}
writeOut(s.outDir / "problems.txt", problems.empty() ? "No problems.\n" : problems);
if (res.hasTree) {
writeOut(s.outDir / "tree.txt", lispCompact(res.tree) + "\n");
if (res.nodeCount <= kMaxDiagramNodes) {
crash::setPhase("drawing the diagram");
writeOut(s.outDir / "tree.svg", renderSvg(res.tree, true).svg);
} else {
fs::remove(s.outDir / "tree.svg", ec);
}
} else {
fs::remove(s.outDir / "tree.txt", ec);
fs::remove(s.outDir / "tree.svg", ec);
}
crash::setPhase("writing the report");
writeReport(s, rin);
// ---- console
if (s.opt.tokens) {
for (const TokenInfo& t : res.tokens) std::printf("%s\n", tokenLine(t, total).c_str());
}
if (res.hasTree && !s.opt.quiet) std::printf("%s", lispPretty(res.tree).c_str());
std::fflush(stdout);
for (const Diagnostic& d : res.diagnostics) printDiagnostic(con, d, name, lines);
std::string verdict;
if (errors > 0) verdict = con.eRed(plural(errors, "syntax error"));
else if (warnings > 0) verdict = con.eYellow(rin.statusText);
else verdict = con.eGreen(rin.statusText);
std::string tail = "(" + plural(realTokens, "token");
if (res.hasTree) tail += ", " + plural(res.nodeCount, "node");
tail += ")";
std::fprintf(stderr, "%s %s\n", verdict.c_str(), con.eDim(tail).c_str());
if (!s.opt.quiet) {
std::fprintf(stderr, "%s\n", con.eDim("Report: " + pathToUtf8(s.outDir / "report.html")).c_str());
}
return errors > 0 ? 1 : 0;
}
// One full pass: read the input, analyze, write every output, print a summary.
int runOnce(Session& s, const Console& con) {
std::error_code ec;
fs::create_directories(s.outDir, ec);
if (ec) {
std::fprintf(stderr, "antlrlab: cannot create %s: %s\n", pathToUtf8(s.outDir).c_str(), ec.message().c_str());
return 2;
}
std::string raw, name, inErr;
ReportInput rin;
if (!readInput(s, raw, name, inErr)) {
std::fprintf(stderr, "antlrlab: %s\n", inErr.c_str());
return 2;
}
const std::string text = sanitizeUtf8(raw);
++s.version;
rin.input = text;
rin.hasParser = s.grammarOk && s.spec.hasParser;
// ---- the grammar did not compile
if (!s.grammarOk) {
rin.statusKind = "fail";
rin.statusText = "Grammar has errors";
rin.statusDetail = s.grammarError;
rin.noDiagram = "The grammar could not be compiled, so there is nothing to draw. See the Grammar tab for the ANTLR output.";
rin.meta = {};
writeReport(s, rin);
removeStaleOutputs(s);
writeOut(s.outDir / "problems.txt", s.grammarError + "\n");
std::fflush(stdout);
if (!s.toolLog.empty()) std::fprintf(stderr, "%s", s.toolLog.c_str());
std::fprintf(stderr, "%s %s\n", con.eRed("Grammar has errors:").c_str(), s.grammarError.c_str());
return 2;
}
// ---- the part that uses the ANTLR runtime, in a worker that may crash or time out
const Outcome o = runIsolated(
[&](int& code) -> std::string {
code = analyzeAndWrite(s, con, text, name, rin);
return std::string();
},
s.opt.timeout, s.opt.isolate);
if (o.kind == Outcome::Kind::Ok) return o.exitCode;
const std::string msg = describe(o);
++s.version; // the worker may have written a report for this run already
rin.statusKind = "fail";
rin.statusText = o.kind == Outcome::Kind::Crashed ? "The run crashed"
: o.kind == Outcome::Kind::TimedOut ? "The run timed out"
: "The run failed";
rin.statusDetail = msg;
rin.noDiagram = msg;
writeReport(s, rin);
removeStaleOutputs(s);
std::fflush(stdout);
std::fprintf(stderr, "%s %s\n", con.eRed(rin.statusText + ":").c_str(), msg.c_str());
return 2;
}
// --------------------------------------------------------------- watching ----
using Stamps = std::map<std::string, long long>;
long long stampOf(const fs::path& p) {
std::error_code ec;
const auto t = fs::last_write_time(p, ec);
if (ec) return -1;
return static_cast<long long>(t.time_since_epoch().count());
}
Stamps grammarStamps(const Session& s) {
Stamps m;
for (const std::string& g : s.opt.grammars) m[g] = stampOf(pathFromUtf8(g));
if (!s.opt.libDir.empty() && isDirectory(s.opt.libDir)) {
std::error_code ec;
for (fs::directory_iterator it(pathFromUtf8(s.opt.libDir), ec), end; !ec && it != end; it.increment(ec)) {
const std::string ext = lowerCase(pathToUtf8(it->path().extension()));
if (ext == ".g4" || ext == ".tokens") m[pathToUtf8(it->path())] = stampOf(it->path());
}
}
return m;
}
Stamps inputStamps(const Session& s) {
Stamps m;
if (!s.opt.hasText && s.opt.inputFile != "-") m[s.opt.inputFile] = stampOf(pathFromUtf8(s.opt.inputFile));
return m;
}
void openTarget(const std::string& p) { openInBrowser(p); }
int watchLoop(Session& s, const Console& con, int firstCode) {
std::fprintf(stderr, "%s\n", con.eDim("Watching for changes. Press Ctrl+C to stop.").c_str());
Stamps gram = grammarStamps(s), inp = inputStamps(s);
int code = firstCode;
for (;;) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
Stamps g2 = grammarStamps(s), i2 = inputStamps(s);
if (g2 == gram && i2 == inp) continue;
// Let editors finish writing (save-and-rename, formatters) before reading.
for (int settle = 0; settle < 8; ++settle) {
std::this_thread::sleep_for(std::chrono::milliseconds(120));
Stamps g3 = grammarStamps(s), i3 = inputStamps(s);
if (g3 == g2 && i3 == i2) break;
g2 = std::move(g3);
i2 = std::move(i3);
}
const bool grammarChanged = g2 != gram;
gram = g2;
inp = i2;
std::fprintf(stderr, "\n%s\n", con.eDim(grammarChanged ? "--- grammar changed, recompiling ---" : "--- input changed ---").c_str());
if (grammarChanged) compileGrammar(s);
code = runOnce(s, con);
}
return code;
}
// Terminal text of a tree, concatenated: "1+2+3" for the self-test input.
void leafText(const Node& n, std::string& out) {
if (n.kind != Node::Kind::Rule) {
if (n.typeName != "EOF") out += n.label;
return;
}
for (const Node& c : n.children) leafText(c, out);
}
// Runs a tiny left-recursive grammar through the whole pipeline: the ANTLR
// tool, the .interp files, the runtime's interpreters and the tree copy.
int selfTest(Session& s) {
std::printf("antlrlab %s, built for ANTLR %s\n", LAB_VERSION, LAB_ANTLR_VERSION);
#if defined(__clang_version__)
std::printf(" compiler clang %s\n", __clang_version__);
#elif defined(_MSC_VER)
std::printf(" compiler MSVC %d\n", _MSC_VER);
#elif defined(__VERSION__)
std::printf(" compiler %s\n", __VERSION__);
#endif
#if defined(_WIN32)
const char* platformName = "Windows";
#elif defined(__APPLE__)
const char* platformName = "macOS";
#elif defined(__linux__)
const char* platformName = "Linux";
#else
const char* platformName = "POSIX";
#endif
std::printf(" platform %s\n", platformName);
if (!kHasIsolation) std::printf(" isolation not available on this platform (the runtime runs in this process)\n");
else std::printf(" isolation %s\n", s.opt.isolate ? "on (runtime runs in a child process)" : "off");
std::printf(" ANTLR tool %s\n", s.tool.found ? s.tool.description.c_str() : "NOT FOUND");
if (!s.tool.found) {
std::printf("\nFAIL: %s\n", s.tool.error.c_str());
return 2;
}
const fs::path dir = s.outDir / "selftest";
std::error_code ec;
fs::remove_all(dir, ec);
fs::create_directories(dir, ec);
const fs::path g = dir / "SelfTest.g4";
writeFile(g,
"grammar SelfTest;\n"
"prog : expr EOF ;\n"
"expr : expr '+' expr | INT ;\n"
"INT : [0-9]+ ;\n"
"WS : [ ]+ -> skip ;\n");
const CompileResult r = compileGrammars(s.tool, {g}, "", dir / "gen", dir / "tool.log");
if (!r.ok) {
std::printf("\nFAIL: the ANTLR tool could not compile the test grammar: %s\n%s", r.summary.c_str(), r.log.c_str());
return 2;
}
std::printf(" [ok] the ANTLR tool compiled the test grammar\n");
std::string err;
if (!loadInterps(r.interps, s.spec, err)) {
std::printf("\nFAIL: %s\n", err.c_str());
return 2;
}
std::printf(" [ok] read %zu .interp files (%zu lexer rules, %zu parser rules, ATN of %zu + %zu numbers)\n",
r.interps.size(), s.spec.lexer.ruleNames.size(), s.spec.parser.ruleNames.size(),
s.spec.lexer.atn.size(), s.spec.parser.atn.size());
const Outcome o = runIsolated(
[&](int& code) -> std::string {
Model model;
std::string e;
if (!buildModel(s.spec, model, e)) {
code = 2;
return "could not load the grammar into the runtime: " + e;
}
AnalysisResult res;
if (!analyze(model, "1 + 2 + 3", "", res, e)) {
code = 2;
return e;
}
std::string leaves;
if (res.hasTree) leafText(res.tree, leaves);
if (res.errorCount() != 0 || !res.hasTree || leaves != "1+2+3") {
code = 1;
return "unexpected result: " + plural(res.errorCount(), "error") + ", tree: " +
(res.hasTree ? lispCompact(res.tree) : std::string("(none)"));
}
code = 0;
return lispCompact(res.tree);
},
s.opt.timeout, s.opt.isolate);
if (o.kind != Outcome::Kind::Ok) {
std::printf("\nFAIL: %s\n", describe(o).c_str());
return 2;
}
if (o.exitCode != 0) {
std::printf("\nFAIL: %s\n", o.payload.c_str());
return 2;
}
std::printf(" [ok] the runtime parsed \"1 + 2 + 3\": %s\n", o.payload.c_str());
std::printf("\nPASS: the ANTLR tool and the runtime work together on this machine.\n"
"If a grammar of yours still crashes, the problem is specific to it.\n");
return 0;
}
int realMain(const std::vector<std::string>& args) {
initConsole();
crash::initShared();
crash::installHandlers();
crash::AltStack altStack;
Session s;
const int parsed = parseArgs(args, s.opt);
if (parsed >= 0) return parsed;
if (!envVar("ANTLRLAB_NO_ISOLATE").empty()) s.opt.isolate = false;
Console con;
const bool noColorEnv = !envVar("NO_COLOR").empty();
con.out = s.opt.color && !noColorEnv && terminalSupportsColor(stdout);
con.err = s.opt.color && !noColorEnv && terminalSupportsColor(stderr);
if (s.opt.serve) {
ServeOptions so;
so.root = s.opt.root.empty() ? fs::current_path() : pathFromUtf8(s.opt.root);
so.port = s.opt.port;
so.timeoutSeconds = s.opt.timeout;
so.isolate = s.opt.isolate;
so.open = s.opt.grammars;
so.open.insert(so.open.end(), s.opt.inputs.begin(), s.opt.inputs.end());
if (s.opt.open) so.openBrowser = [](const std::string& url) { openTarget(url); };
return runServer(so, locateTool(s.opt.jar));
}
s.outDir = pathFromUtf8(s.opt.outDir);
s.genDir = s.outDir / "generated";
std::error_code ec;
fs::create_directories(s.outDir, ec);
s.tool = locateTool(s.opt.jar);
if (s.opt.selftest) return selfTest(s);
if (!s.tool.found) {
std::fprintf(stderr, "antlrlab: %s\n", s.tool.error.c_str());
return 2;
}
compileGrammar(s);
int code = runOnce(s, con);
s.firstRun = false;
if (s.opt.open) {
std::error_code abs;
openTarget(pathToUtf8(fs::absolute(s.outDir / "report.html", abs)));
}
if (s.opt.watch) return watchLoop(s, con, code);
return code;
}
} // namespace
int main(int argc, char** argv) {
try {
return realMain(commandLineArgs(argc, argv));
} catch (const std::exception& e) {
std::fprintf(stderr, "antlrlab: unexpected error: %s\n", e.what());
return 2;
}
}
+57
View File
@@ -0,0 +1,57 @@
// Shared by the command line and the web server: turns an AnalysisResult into
// the verdict (status text, detail line, short stats) shown to the user.
#pragma once
#include <string>
#include "report.hpp"
#include "types.hpp"
#include "util.hpp"
namespace lab {
inline size_t countRealTokens(const AnalysisResult& res) {
if (res.tokens.empty()) return 0;
return res.tokens.size() - (res.tokens.back().isEof ? 1 : 0);
}
// Fills status, detail, meta and result of `rin` (which points at `res`). `hasParser` says whether the
// grammar has parser rules (a lexer-only grammar only tokenizes).
inline void fillReportStatus(ReportInput& rin, const AnalysisResult& res, bool hasParser) {
const size_t errors = res.errorCount();
const size_t warnings = res.warningCount();
rin.haveResult = true;
rin.hasParser = hasParser;
rin.startRule = res.startRule;
if (errors > 0) {
rin.statusKind = "error";
rin.statusText = plural(errors, "syntax error");
} else if (warnings > 0) {
rin.statusKind = "warn";
rin.statusText = "Input is valid, with " + plural(warnings, "warning");
} else {
rin.statusKind = "ok";
rin.statusText = hasParser ? "Input is valid" : "Input tokenized";
}
rin.statusDetail.clear();
for (const Diagnostic& d : res.diagnostics) {
if ((errors > 0) == (d.severity == Diagnostic::Severity::Error)) {
rin.statusDetail = (d.line ? std::to_string(d.line) + ":" + std::to_string(d.col) + " " : "") + d.message;
break;
}
}
if (rin.statusDetail.empty() && !hasParser) {
rin.statusDetail = "This grammar has no parser rules, so there is no tree.";
}
rin.meta.clear();
rin.meta.push_back(plural(countRealTokens(res), "token"));
if (res.hasTree) rin.meta.push_back(plural(res.nodeCount, "tree node"));
rin.meta.push_back("lexed in " + fixed1(res.lexMs) + " ms");
if (hasParser) rin.meta.push_back("parsed in " + fixed1(res.parseMs) + " ms");
rin.result = &res; // not copied: the caller keeps `res` alive while the report is built
}
} // namespace lab
+449
View File
@@ -0,0 +1,449 @@
// Everything that differs between operating systems, in one place.
//
// Supported: Linux, macOS and other POSIX systems (GCC, Clang) and Windows
// (MSVC, clang-cl, MinGW-w64 with POSIX threads).
//
// * UTF-8 <-> filesystem paths. On Windows a std::string passed to
// std::filesystem::path is read in the ANSI code page, not as UTF-8, so
// every path that comes from the command line, the browser or the screen
// goes through pathFromUtf8() / pathToUtf8().
// * command line, environment variables, console set-up, random bytes
// * starting a program (the ANTLR tool, the web browser) without a shell
//
// NOTE: <cstdio> must come before the ANTLR runtime headers (they #undef the
// EOF macro, which only sticks if it was defined already), and windows.h
// must come before them too, because it defines macros (ERROR, ...) that the
// runtime uses as names. Every file includes this one first (via util.hpp).
#pragma once
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <random>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <bcrypt.h>
#include <fcntl.h>
#include <io.h>
#include <shellapi.h>
#ifdef ERROR
#undef ERROR // from wingdi.h; clashes with ParseTreeType::ERROR in the ANTLR runtime
#endif
#ifdef _MSC_VER
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "dbghelp.lib")
#endif
#else
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#include <cerrno>
#include <fcntl.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
extern char** environ;
#endif
namespace lab {
namespace fs = std::filesystem;
// ----------------------------------------------------------- UTF-8 paths ----
#ifdef _WIN32
inline std::wstring widen(const std::string& s) {
if (s.empty()) return std::wstring();
const int n = ::MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
if (n <= 0) return std::wstring();
std::wstring w(static_cast<size_t>(n), L'\0');
::MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), &w[0], n);
return w;
}
inline std::string narrow(const std::wstring& w) {
if (w.empty()) return std::string();
const int n = ::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
if (n <= 0) return std::string();
std::string s(static_cast<size_t>(n), '\0');
::WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), &s[0], n, nullptr, nullptr);
return s;
}
#endif
// UTF-8 text -> path, and back. Use these instead of fs::path(std::string)
// and path.string(), which are wrong for non-ASCII names on Windows.
inline fs::path pathFromUtf8(const std::string& s) {
#ifdef _WIN32
return fs::path(widen(s));
#else
return fs::path(s);
#endif
}
inline std::string pathToUtf8(const fs::path& p) {
#ifdef _WIN32
return narrow(p.native());
#else
return p.native();
#endif
}
// Same, but with '/' as the separator (what the browser sees).
inline std::string pathToGenericUtf8(const fs::path& p) {
#ifdef _WIN32
return narrow(p.generic_wstring());
#else
return p.generic_string();
#endif
}
// ------------------------------------------------ command line, environment ----
// The program's arguments as UTF-8, whatever the platform's own encoding is.
inline std::vector<std::string> commandLineArgs(int argc, char** argv) {
std::vector<std::string> out;
#ifdef _WIN32
(void)argc;
(void)argv;
int n = 0;
if (LPWSTR* w = ::CommandLineToArgvW(::GetCommandLineW(), &n)) {
for (int i = 0; i < n; ++i) out.push_back(narrow(w[i]));
::LocalFree(w);
return out;
}
#endif
for (int i = 0; i < argc; ++i) out.emplace_back(argv[i]);
return out;
}
// Value of an environment variable as UTF-8, "" if unset.
inline std::string getEnv(const char* name) {
#ifdef _WIN32
const std::wstring wname = widen(name);
const DWORD n = ::GetEnvironmentVariableW(wname.c_str(), nullptr, 0);
if (n == 0) return std::string();
std::wstring v(n, L'\0');
const DWORD got = ::GetEnvironmentVariableW(wname.c_str(), &v[0], n);
v.resize(got);
return narrow(v);
#else
const char* v = std::getenv(name);
return v ? v : "";
#endif
}
// Full path of the running program, or an empty path if the system will not say.
inline fs::path executablePath() {
#ifdef _WIN32
std::vector<wchar_t> buf(32768);
const DWORD n = ::GetModuleFileNameW(nullptr, buf.data(), static_cast<DWORD>(buf.size()));
if (n == 0 || n >= buf.size()) return fs::path();
return fs::path(std::wstring(buf.data(), n));
#elif defined(__APPLE__)
char buf[4096];
uint32_t size = sizeof buf;
if (::_NSGetExecutablePath(buf, &size) != 0) return fs::path();
std::error_code ec;
fs::path p = fs::weakly_canonical(fs::path(buf), ec);
return ec ? fs::path(buf) : p;
#else
std::error_code ec;
const fs::path p = fs::read_symlink("/proc/self/exe", ec); // Linux; empty elsewhere
return ec ? fs::path() : p;
#endif
}
// ---------------------------------------------------------------- console ----
#ifdef _WIN32
namespace detail {
inline UINT& savedConsoleCodePage() {
static UINT cp = 0;
return cp;
}
inline void restoreConsoleCodePage() {
if (savedConsoleCodePage() != 0) ::SetConsoleOutputCP(savedConsoleCodePage());
}
} // namespace detail
#endif
// Call once at start-up. Windows: print UTF-8 correctly in the console and
// read standard input as raw bytes (no CRLF translation, no Ctrl+Z).
inline void initConsole() {
#ifdef _WIN32
const UINT cp = ::GetConsoleOutputCP();
if (cp != 0 && cp != CP_UTF8) {
detail::savedConsoleCodePage() = cp;
::SetConsoleOutputCP(CP_UTF8);
std::atexit(detail::restoreConsoleCodePage);
}
_setmode(_fileno(stdin), _O_BINARY);
#endif
}
// True when f is a terminal that understands ANSI color codes.
inline bool terminalSupportsColor(std::FILE* f) {
#ifdef _WIN32
if (!_isatty(_fileno(f))) return false;
const HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(_fileno(f)));
DWORD mode = 0;
if (h == INVALID_HANDLE_VALUE || !::GetConsoleMode(h, &mode)) return false;
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif
if (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) return true;
return ::SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; // fails before Windows 10
#else
return ::isatty(::fileno(f)) != 0;
#endif
}
// The Java tool reads its arguments in the Windows ANSI code page, so a path
// with characters outside it (say Cyrillic on a Western system) arrives
// garbled. The short 8.3 form of an existing path is plain ASCII.
// Elsewhere (and for ASCII paths) the path is returned unchanged.
inline std::string toolSafePath(const std::string& utf8Path) {
#ifdef _WIN32
bool ascii = true;
for (unsigned char c : utf8Path) ascii = ascii && c < 0x80;
if (ascii) return utf8Path;
const std::wstring wide = widen(utf8Path);
const DWORD n = ::GetShortPathNameW(wide.c_str(), nullptr, 0);
if (n == 0) return utf8Path;
std::wstring shortPath(n, L'\0');
const DWORD got = ::GetShortPathNameW(wide.c_str(), &shortPath[0], n);
if (got == 0 || got >= n) return utf8Path;
shortPath.resize(got);
const std::string result = narrow(shortPath);
for (unsigned char c : result) {
if (c >= 0x80) return utf8Path; // 8.3 names are switched off on this volume
}
return result;
#else
return utf8Path;
#endif
}
// ----------------------------------------------------------------- random ----
// Cryptographically secure random bytes (the web UI's access token).
inline bool secureRandomBytes(void* buf, size_t n) {
#ifdef _WIN32
return BCryptGenRandom(nullptr, static_cast<PUCHAR>(buf), static_cast<ULONG>(n),
BCRYPT_USE_SYSTEM_PREFERRED_RNG) >= 0;
#else
bool ok = false;
if (std::FILE* f = std::fopen("/dev/urandom", "rb")) {
ok = std::fread(buf, 1, n, f) == n;
std::fclose(f);
}
if (!ok) {
std::random_device rd;
unsigned char* p = static_cast<unsigned char*>(buf);
for (size_t i = 0; i < n; ++i) p[i] = static_cast<unsigned char>(rd());
ok = true;
}
return ok;
#endif
}
// -------------------------------------------------------------- processes ----
struct ProcessResult {
bool started = false;
int exitCode = 127; // 127: could not be started (the shell's convention)
std::string error; // why it could not be started
};
#ifdef _WIN32
namespace detail {
// Quotes one argument the way CommandLineToArgvW / the C runtime read it back.
inline std::wstring quoteWinArg(const std::wstring& arg) {
if (!arg.empty() && arg.find_first_of(L" \t\n\v\"") == std::wstring::npos) return arg;
std::wstring out = L"\"";
for (auto it = arg.begin();; ++it) {
size_t backslashes = 0;
while (it != arg.end() && *it == L'\\') {
++it;
++backslashes;
}
if (it == arg.end()) {
out.append(backslashes * 2, L'\\');
break;
}
if (*it == L'"') out.append(backslashes * 2 + 1, L'\\');
else out.append(backslashes, L'\\');
out.push_back(*it);
}
out.push_back(L'"');
return out;
}
inline bool isBatchFile(const std::string& program) {
if (program.size() < 4) return false;
std::string ext = program.substr(program.size() - 4);
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return ext == ".bat" || ext == ".cmd";
}
inline std::string lastErrorText(DWORD code) {
wchar_t* msg = nullptr;
const DWORD n = ::FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, code, 0, reinterpret_cast<LPWSTR>(&msg), 0, nullptr);
std::string s = "error " + std::to_string(code);
if (n > 0 && msg) {
std::string t = narrow(std::wstring(msg, n));
while (!t.empty() && (t.back() == '\n' || t.back() == '\r' || t.back() == ' ' || t.back() == '.')) t.pop_back();
if (!t.empty()) s = t;
}
if (msg) ::LocalFree(msg);
return s;
}
} // namespace detail
#endif
// Runs a program without a shell, waits for it, and returns its exit code.
// argv[0] is looked up on PATH. Standard output and standard error go to
// logFile (created or truncated); when logFile is empty they are inherited.
// Standard input is empty.
inline ProcessResult runProcess(const std::vector<std::string>& argv, const fs::path& logFile) {
ProcessResult r;
if (argv.empty()) {
r.error = "no program given";
return r;
}
#ifdef _WIN32
std::wstring cmd;
if (detail::isBatchFile(argv[0])) {
// .bat / .cmd files only run through cmd.exe; /s keeps the outer quotes apart from the inner ones.
cmd = L"cmd.exe /d /s /c \"";
for (size_t i = 0; i < argv.size(); ++i) {
if (i) cmd += L' ';
cmd += detail::quoteWinArg(widen(argv[i]));
}
cmd += L'"';
} else {
for (size_t i = 0; i < argv.size(); ++i) {
if (i) cmd += L' ';
cmd += detail::quoteWinArg(widen(argv[i]));
}
}
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof sa;
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = TRUE;
HANDLE nul = ::CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
HANDLE log = INVALID_HANDLE_VALUE;
if (!logFile.empty()) {
log = ::CreateFileW(logFile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (log == INVALID_HANDLE_VALUE) {
r.error = "cannot create " + pathToUtf8(logFile) + ": " + detail::lastErrorText(::GetLastError());
if (nul != INVALID_HANDLE_VALUE) ::CloseHandle(nul);
return r;
}
}
STARTUPINFOW si;
std::memset(&si, 0, sizeof si);
si.cb = sizeof si;
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = nul;
si.hStdOutput = log != INVALID_HANDLE_VALUE ? log : ::GetStdHandle(STD_OUTPUT_HANDLE);
si.hStdError = log != INVALID_HANDLE_VALUE ? log : ::GetStdHandle(STD_ERROR_HANDLE);
PROCESS_INFORMATION pi;
std::memset(&pi, 0, sizeof pi);
std::vector<wchar_t> buf(cmd.begin(), cmd.end());
buf.push_back(L'\0'); // CreateProcessW may modify the command line
const BOOL ok = ::CreateProcessW(nullptr, buf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr,
&si, &pi);
const DWORD err = ok ? 0 : ::GetLastError();
if (log != INVALID_HANDLE_VALUE) ::CloseHandle(log);
if (nul != INVALID_HANDLE_VALUE) ::CloseHandle(nul);
if (!ok) {
r.error = detail::lastErrorText(err);
return r;
}
r.started = true;
::WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
::GetExitCodeProcess(pi.hProcess, &code);
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
r.exitCode = static_cast<int>(code);
return r;
#else
std::vector<char*> args;
for (const std::string& a : argv) args.push_back(const_cast<char*>(a.c_str()));
args.push_back(nullptr);
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0);
const std::string logPath = logFile.native();
if (!logPath.empty()) {
posix_spawn_file_actions_addopen(&actions, 1, logPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
posix_spawn_file_actions_adddup2(&actions, 1, 2);
}
pid_t pid = 0;
const int rc = posix_spawnp(&pid, args[0], &actions, nullptr, args.data(), environ);
posix_spawn_file_actions_destroy(&actions);
if (rc != 0) {
r.error = std::strerror(rc);
return r;
}
r.started = true;
int status = 0;
while (::waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
r.exitCode = -1;
return r;
}
}
if (WIFEXITED(status)) r.exitCode = WEXITSTATUS(status);
else if (WIFSIGNALED(status)) r.exitCode = 128 + WTERMSIG(status);
else r.exitCode = -1;
return r;
#endif
}
// Opens a file or a URL in the default application (the browser). Does not wait.
inline void openInBrowser(const std::string& target) {
#ifdef _WIN32
::ShellExecuteW(nullptr, L"open", widen(target).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
#else
#ifdef __APPLE__
const char* opener = "open";
#else
const char* opener = "xdg-open";
#endif
std::thread([opener, target]() { runProcess({opener, target}, fs::path("/dev/null")); }).detach();
#endif
}
} // namespace lab
+331
View File
@@ -0,0 +1,331 @@
// Turns a lab::Node tree into (a) LISP-style text like ANTLR's toStringTree and
// (b) an SVG diagram. Pure C++ - no ANTLR types in here.
#pragma once
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include "types.hpp"
#include "util.hpp"
namespace lab {
// ------------------------------------------------------------ LISP text ----
inline void lispCompact(const Node& n, std::string& out) {
if (n.kind != Node::Kind::Rule) {
out += visible(n.label);
return;
}
if (n.children.empty()) {
out += n.label;
return;
}
out += '(';
out += n.label;
for (const Node& c : n.children) {
out += ' ';
lispCompact(c, out);
}
out += ')';
}
inline std::string lispCompact(const Node& n) {
std::string s;
lispCompact(n, s);
return s;
}
// Indented form: a subtree stays on one line when it fits, otherwise each
// child goes on its own line.
namespace detail {
struct LispInfo {
size_t len = 0; // length of the one-line form
size_t count = 1; // nodes in the subtree
};
// Fills `v` in pre-order, so that a node's entry is at its pre-order index.
inline LispInfo lispInfo(const Node& n, std::vector<LispInfo>& v) {
const size_t me = v.size();
v.emplace_back();
LispInfo info;
if (n.kind != Node::Kind::Rule) {
info.len = visible(n.label).size();
} else if (n.children.empty()) {
info.len = n.label.size();
} else {
info.len = 2 + n.label.size(); // "(" label ... ")"
for (const Node& c : n.children) {
const LispInfo ci = lispInfo(c, v);
info.len += 1 + ci.len;
info.count += ci.count;
}
}
v[me] = info;
return info;
}
// Indentation stops growing after this many levels. Without a cap a tree that
// is N levels deep (a long right-recursive list, say) prints O(N^2) spaces.
constexpr size_t kMaxIndentLevels = 40;
inline void lispPrettyRec(const Node& n, size_t depth, const std::vector<LispInfo>& v, size_t& next,
std::string& out) {
const size_t me = next++;
const LispInfo info = v[me];
const size_t pad = std::min<size_t>(depth, kMaxIndentLevels) * 2; // capped: see below
out.append(pad, ' ');
if (n.kind != Node::Kind::Rule || n.children.empty() || pad + info.len <= 88) {
lispCompact(n, out);
out += '\n';
next = me + info.count; // skip the descendants, they were printed inline
return;
}
out += '(';
out += n.label;
out += '\n';
for (const Node& c : n.children) lispPrettyRec(c, depth + 1, v, next, out);
out.pop_back(); // the newline after the last child
out += ")\n";
}
} // namespace detail
inline std::string lispPretty(const Node& n) {
std::vector<detail::LispInfo> info;
info.reserve(64);
detail::lispInfo(n, info);
std::string s;
size_t next = 0;
detail::lispPrettyRec(n, 0, info, next, s);
return s;
}
// ------------------------------------------------------------------ SVG ----
// Palette. Light and dark values are used both for the standalone .svg file
// (light, literal colors) and for the HTML report (CSS variables, themeable).
struct SvgColor {
const char* name;
const char* light;
const char* dark;
};
inline const std::vector<SvgColor>& svgPalette() {
static const std::vector<SvgColor> p = {
{"bg", "#fbfbfc", "#0f1b2d"}, {"text", "#1b2a41", "#e8eef7"},
{"edge", "#9aa7b8", "#4a5d78"}, {"rulefill", "#ffffff", "#132238"},
{"rulestroke", "#1b2a41", "#9db8e3"}, {"tokfill", "#1b2a41", "#dbe7fb"},
{"toktext", "#f4f7fb", "#0f1b2d"}, {"toksub", "#a9b8cc", "#4b5e7a"},
{"errfill", "#d1352b", "#f0625a"}, {"errtext", "#ffffff", "#1a0b0a"},
};
return p;
}
// Variable declarations for the .al-tree element, e.g. "--al-bg:#fff;...".
inline std::string svgVarDecls(bool dark) {
std::string s;
for (const auto& c : svgPalette()) {
s += std::string("--al-") + c.name + ":" + (dark ? c.dark : c.light) + ";";
}
return s;
}
inline std::string svgStyle(bool useVars) {
auto col = [&](const char* name) -> std::string {
if (useVars) return std::string("var(--al-") + name + ")";
for (const auto& c : svgPalette()) {
if (std::string(c.name) == name) return c.light;
}
return "#000";
};
std::string s;
s += ".al-tree{font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono','Liberation Mono',monospace}";
s += ".al-tree .al-bg{fill:" + col("bg") + "}";
s += ".al-tree .al-edge{fill:none;stroke:" + col("edge") + ";stroke-width:1.4}";
s += ".al-tree .al-box{stroke-width:1.5}";
s += ".al-tree .al-rule .al-box{fill:" + col("rulefill") + ";stroke:" + col("rulestroke") + "}";
s += ".al-tree .al-term .al-box{fill:" + col("tokfill") + ";stroke:" + col("tokfill") + "}";
s += ".al-tree .al-error .al-box{fill:" + col("errfill") + ";stroke:" + col("errfill") + "}";
s += ".al-tree .al-label{font-size:13px;fill:" + col("text") + "}";
s += ".al-tree .al-term .al-label{fill:" + col("toktext") + "}";
s += ".al-tree .al-error .al-label{fill:" + col("errtext") + "}";
s += ".al-tree .al-type{font-size:10px;fill:" + col("toksub") + "}";
s += ".al-tree .al-error .al-type{fill:" + col("errtext") + ";opacity:.8}";
s += ".al-tree .al-node:hover .al-box{stroke-width:3}";
return s;
}
namespace detail {
struct Item {
const Node* node = nullptr;
int depth = 0;
double w = 0, h = 0;
double subtree = 0, left = 0, cx = 0;
std::vector<int> kids;
};
constexpr double kCharW = 7.8; // 13px monospace
constexpr double kTypeCharW = 6.2; // 10px monospace
constexpr double kPadX = 12;
constexpr double kHGap = 14;
constexpr double kMargin = 24;
constexpr double kRuleH = 30;
constexpr double kTermH = 44;
constexpr double kRowPitch = 76;
inline std::string shownLabel(const Node& n) {
if (n.kind == Node::Kind::Rule) return n.label;
return truncateCp(visible(n.label), 28);
}
inline void collect(const Node& n, int parent, int depth, std::vector<Item>& items) {
const int idx = static_cast<int>(items.size());
Item it;
it.node = &n;
it.depth = depth;
const double textW = static_cast<double>(cpCount(shownLabel(n))) * kCharW;
if (n.kind == Node::Kind::Rule) {
it.w = textW + 2 * kPadX;
it.h = kRuleH;
} else {
const double typeW = static_cast<double>(cpCount(n.typeName)) * kTypeCharW;
it.w = std::max(textW, typeW) + 2 * kPadX;
it.h = kTermH;
}
it.w = std::max(it.w, 36.0);
items.push_back(std::move(it));
if (parent >= 0) items[static_cast<size_t>(parent)].kids.push_back(idx);
for (const Node& c : n.children) collect(c, idx, depth + 1, items);
}
inline std::string num(double v) { return fixed1(v); }
} // namespace detail
struct SvgResult {
std::string svg;
double width = 0;
double height = 0;
};
// Tidy top-down layout: every subtree gets a slot as wide as its widest row,
// children sit side by side inside that slot, and a parent is centred over the
// span of its children.
//
// standalone = true -> XML prolog, embedded <style>, opaque background
// standalone = false -> bare <svg> for embedding in the HTML report, which
// supplies the CSS (and lets its own canvas show through)
inline SvgResult renderSvg(const Node& root, bool standalone) {
using namespace detail;
std::vector<Item> items;
collect(root, -1, 0, items);
const size_t N = items.size();
for (size_t i = N; i-- > 0;) { // children come after parents, so go backwards
Item& it = items[i];
if (it.kids.empty()) {
it.subtree = it.w;
} else {
double sum = kHGap * static_cast<double>(it.kids.size() - 1);
for (int k : it.kids) sum += items[static_cast<size_t>(k)].subtree;
it.subtree = std::max(it.w, sum);
}
}
items[0].left = kMargin;
for (size_t i = 0; i < N; ++i) {
Item& it = items[i];
if (it.kids.empty()) continue;
double sum = kHGap * static_cast<double>(it.kids.size() - 1);
for (int k : it.kids) sum += items[static_cast<size_t>(k)].subtree;
double x = it.left + (it.subtree - sum) / 2;
for (int k : it.kids) {
Item& kid = items[static_cast<size_t>(k)];
kid.left = x;
x += kid.subtree + kHGap;
}
}
int maxDepth = 0;
for (size_t i = N; i-- > 0;) {
Item& it = items[i];
maxDepth = std::max(maxDepth, it.depth);
if (it.kids.empty()) {
it.cx = it.left + it.subtree / 2;
} else {
const double mid = (items[static_cast<size_t>(it.kids.front())].cx +
items[static_cast<size_t>(it.kids.back())].cx) / 2;
const double lo = it.left + it.w / 2;
const double hi = it.left + it.subtree - it.w / 2;
it.cx = std::min(std::max(mid, lo), hi);
}
}
SvgResult res;
res.width = items[0].subtree + 2 * kMargin;
res.height = kMargin * 2 + maxDepth * kRowPitch + kTermH;
auto yOf = [&](const Item& it) { return kMargin + it.depth * kRowPitch; };
std::string& s = res.svg;
s.reserve(N * 260 + 2048);
if (standalone) s += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
s += "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"al-tree\" width=\"" + num(res.width) +
"\" height=\"" + num(res.height) + "\" viewBox=\"0 0 " + num(res.width) + " " +
num(res.height) + "\" role=\"img\" aria-label=\"Parse tree\">\n";
if (standalone) {
s += "<style>" + svgStyle(false) + "</style>\n";
s += "<rect class=\"al-bg\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\"/>\n";
}
// Edges first so that the boxes sit on top of them.
for (size_t p = 0; p < N; ++p) {
const Item& par = items[p];
for (int k : par.kids) {
const Item& kid = items[static_cast<size_t>(k)];
const double px = par.cx, py = yOf(par) + par.h;
const double cx = kid.cx, cy = yOf(kid);
const double my = (py + cy) / 2;
s += "<path class=\"al-edge\" d=\"M" + num(px) + "," + num(py) + " C" + num(px) + "," +
num(my) + " " + num(cx) + "," + num(my) + " " + num(cx) + "," + num(cy) + "\"/>\n";
}
}
for (const Item& it : items) {
const Node& n = *it.node;
const double x = it.cx - it.w / 2, y = yOf(it);
const char* cls = n.kind == Node::Kind::Rule ? "al-rule"
: n.kind == Node::Kind::Terminal ? "al-term"
: "al-error";
s += std::string("<g class=\"al-node ") + cls + "\"";
if (n.start >= 0 && n.stop >= n.start) {
s += " data-s=\"" + std::to_string(n.start) + "\" data-e=\"" + std::to_string(n.stop) + "\"";
}
s += ">";
if (n.kind == Node::Kind::Rule) {
s += "<title>" + escapeXml(n.label) + "</title>";
s += "<rect class=\"al-box\" x=\"" + num(x) + "\" y=\"" + num(y) + "\" width=\"" +
num(it.w) + "\" height=\"" + num(it.h) + "\" rx=\"" + num(it.h / 2) + "\"/>";
s += "<text class=\"al-label\" x=\"" + num(it.cx) + "\" y=\"" + num(y + it.h / 2 + 4.5) +
"\" text-anchor=\"middle\">" + escapeXml(shownLabel(n)) + "</text>";
} else {
std::string tip = visible(n.label) + "\n" + n.typeName;
if (n.line > 0) tip += " line " + std::to_string(n.line) + ":" + std::to_string(n.col);
if (n.kind == Node::Kind::Error) tip += "\n(error node: token skipped or inserted during recovery)";
s += "<title>" + escapeXml(tip) + "</title>";
s += "<rect class=\"al-box\" x=\"" + num(x) + "\" y=\"" + num(y) + "\" width=\"" +
num(it.w) + "\" height=\"" + num(it.h) + "\" rx=\"3\"/>";
s += "<text class=\"al-label\" x=\"" + num(it.cx) + "\" y=\"" + num(y + 18) +
"\" text-anchor=\"middle\">" + escapeXml(shownLabel(n)) + "</text>";
s += "<text class=\"al-type\" x=\"" + num(it.cx) + "\" y=\"" + num(y + 33) +
"\" text-anchor=\"middle\">" + escapeXml(n.typeName) + "</text>";
}
s += "</g>\n";
}
s += "</svg>\n";
return res;
}
} // namespace lab
+850
View File
@@ -0,0 +1,850 @@
// Builds report.html: one self-contained page (no network, no libraries) that
// shows the parse tree diagram, tree text, tokens, problems and grammar.
//
// The page renders from a JSON object (window.LAB_DATA). In watch mode a
// second file, report-data.js, calls window.labUpdate({...}) with fresh data
// and the page polls for it, so it also works when opened from file://.
//
// The CSS and JS live in separate raw string literals that stay below the
// 16 KB literal limit of MSVC.
#pragma once
#include <string>
#include <vector>
#include "render.hpp"
#include "types.hpp"
#include "util.hpp"
namespace lab {
constexpr size_t kMaxDiagramNodes = 20000;
struct GrammarSource {
std::string name;
std::string text;
};
struct ReportInput {
int version = 1;
bool live = false;
std::string statusKind = "ok"; // ok | warn | error | fail
std::string statusText;
std::string statusDetail;
std::vector<GrammarSource> grammars;
std::string toolLog;
bool hasParser = false;
std::string startRule;
std::string input; // sanitized UTF-8
std::vector<std::string> meta; // short stats, rendered as separate spans
bool haveResult = false;
const AnalysisResult* result = nullptr; // not owned; must outlive reportJson()
std::string noDiagram; // explanation shown instead of a diagram
};
// ------------------------------------------------------------------ JSON ----
inline std::string reportJson(const ReportInput& in) {
std::string j;
j.reserve(in.input.size() * 4 + 4096);
auto str = [&](const std::string& s) { j += jsonString(s); };
auto key = [&](const char* k) {
j += '"';
j += k;
j += "\":";
};
j += '{';
key("version"); j += std::to_string(in.version);
j += ','; key("live"); j += in.live ? "true" : "false";
j += ','; key("status");
j += '{'; key("kind"); str(in.statusKind);
j += ','; key("text"); str(in.statusText);
j += ','; key("detail"); str(in.statusDetail);
j += '}';
j += ','; key("grammars"); j += '[';
for (size_t i = 0; i < in.grammars.size(); ++i) {
if (i) j += ',';
j += '{'; key("name"); str(in.grammars[i].name);
j += ','; key("text"); str(in.grammars[i].text);
j += '}';
}
j += ']';
j += ','; key("toolLog"); str(in.toolLog);
j += ','; key("startRule"); str(in.startRule);
j += ','; key("hasParser"); j += in.hasParser ? "true" : "false";
j += ','; key("input"); str(in.input);
j += ','; key("meta"); j += '[';
for (size_t i = 0; i < in.meta.size(); ++i) {
if (i) j += ',';
str(in.meta[i]);
}
j += ']';
j += ','; key("diagnostics"); j += '[';
if (in.haveResult && in.result) {
for (size_t i = 0; i < in.result->diagnostics.size(); ++i) {
const Diagnostic& d = in.result->diagnostics[i];
if (i) j += ',';
j += '{'; key("sev"); str(d.severity == Diagnostic::Severity::Error ? "error" : "warning");
j += ','; key("src"); str(d.source);
j += ','; key("line"); j += std::to_string(d.line);
j += ','; key("col"); j += std::to_string(d.col);
j += ','; key("msg"); str(d.message);
j += ','; key("s"); j += std::to_string(d.start);
j += ','; key("e"); j += std::to_string(d.stop);
j += '}';
}
}
j += ']';
j += ','; key("tokens"); j += '[';
if (in.haveResult && in.result) {
for (size_t i = 0; i < in.result->tokens.size(); ++i) {
const TokenInfo& t = in.result->tokens[i];
if (i) j += ',';
j += '{'; key("i"); j += std::to_string(t.index);
j += ','; key("t"); str(t.isEof ? "<EOF>" : visible(t.text));
j += ','; key("ty"); str(t.typeName);
j += ','; key("ch"); str(t.channel);
j += ','; key("l"); j += std::to_string(t.line);
j += ','; key("c"); j += std::to_string(t.col);
j += ','; key("s"); j += std::to_string(t.start);
j += ','; key("e"); j += std::to_string(t.stop);
j += '}';
}
}
j += ']';
std::string tree, pretty, svg, noDiagram = in.noDiagram;
if (in.haveResult && in.result && in.result->hasTree) {
tree = lispCompact(in.result->tree);
pretty = lispPretty(in.result->tree);
if (in.result->nodeCount <= kMaxDiagramNodes) {
svg = renderSvg(in.result->tree, false).svg;
} else if (noDiagram.empty()) {
noDiagram = "The tree has " + std::to_string(in.result->nodeCount) +
" nodes, which is too many to draw. The Tree tab and tree.txt still have it.";
}
} else if (noDiagram.empty()) {
noDiagram = in.hasParser ? "No parse tree was produced." : "This grammar has no parser rules, so there is no tree. See the Tokens tab.";
}
j += ','; key("tree"); str(tree);
j += ','; key("treePretty"); str(pretty);
j += ','; key("svg"); str(svg);
j += ','; key("svgStyle"); str(svgStyle(false));
j += ','; key("noDiagram"); str(noDiagram);
j += '}';
return j;
}
// Content of report-data.js (watch mode).
inline std::string reportDataJs(const ReportInput& in) {
return "window.labUpdate && window.labUpdate(" + reportJson(in) + ");\n";
}
// ------------------------------------------------------------------- CSS ----
namespace web {
inline std::string lightVars() {
return "--page:#f5f7fa;--pane:#ffffff;--ink:#1b2a41;--muted:#5b6a80;--line:#d8dfe9;"
"--accent:#2456d3;--ok:#1a7f45;--err:#c62f2a;--warn:#a86a06;"
"--okbg:#e5f4ea;--errbg:#fbe7e5;--warnbg:#fbf0d9;--hl:rgba(36,86,211,.2);"
"--dot:#cdd5e1;" + svgVarDecls(false);
}
inline std::string darkVars() {
return "--page:#0c1626;--pane:#101d31;--ink:#e3ebf7;--muted:#8fa1ba;--line:#223450;"
"--accent:#7aa2ff;--ok:#4cc38a;--err:#ff7b72;--warn:#e3b341;"
"--okbg:#0f2a27;--errbg:#2e1a24;--warnbg:#2b2617;--hl:rgba(122,162,255,.28);"
"--dot:#1d3050;" + svgVarDecls(true);
}
inline std::string css() {
std::string s;
s += ":root{" + lightVars() + "}\n";
s += "@media (prefers-color-scheme:dark){:root:not([data-theme=light]){" + darkVars() + "}}\n";
s += ":root[data-theme=dark]{" + darkVars() + "}\n";
s += R"CSS(
*{box-sizing:border-box}
[hidden]{display:none!important}
html,body{height:100%}
body{margin:0;display:flex;flex-direction:column;background:var(--page);color:var(--ink);
font:14px/1.45 system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',sans-serif}
pre,code,.mono,td.mono{font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono','Liberation Mono',monospace}
button{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.top{display:flex;align-items:center;gap:18px;padding:9px 20px;border-bottom:1px solid var(--line);background:var(--pane)}
.brand{font-weight:650;letter-spacing:.01em;white-space:nowrap}
.files{display:flex;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:13px}
.files span{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.spacer{flex:1}
.live{color:var(--muted);font-size:12.5px}
.btn{white-space:nowrap;padding:4px 11px;border:1px solid var(--line);background:var(--pane);border-radius:4px;cursor:pointer;line-height:1.4}
.btn:hover{border-color:var(--accent)}
.btn[aria-pressed=true]{background:var(--hl);border-color:var(--accent)}
.verdict{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 28px;padding:16px 20px 16px 24px;
border-bottom:1px solid var(--line);border-left:6px solid var(--ok);background:var(--okbg)}
.verdict.warn{border-left-color:var(--warn);background:var(--warnbg)}
.verdict.error,.verdict.fail{border-left-color:var(--err);background:var(--errbg)}
.vhead{margin:0;font-size:clamp(1.5rem,2.6vw,2.15rem);font-weight:650;letter-spacing:-.01em;line-height:1.15;color:var(--ok)}
.verdict.warn .vhead{color:var(--warn)}
.verdict.error .vhead,.verdict.fail .vhead{color:var(--err)}
.vdetail{flex:1 1 280px;min-width:0;margin:0;font-size:13px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.vmeta{display:flex;flex-wrap:wrap;gap:2px 16px;color:var(--muted);font-size:12.5px}
main{flex:1;min-height:0;display:grid;grid-template-columns:minmax(260px,34%) minmax(0,1fr)}
.left{display:flex;flex-direction:column;min-height:0;border-right:1px solid var(--line);background:var(--pane)}
.right{display:flex;flex-direction:column;min-height:0;min-width:0}
.ptitle{padding:9px 16px;border-bottom:1px solid var(--line);font-weight:600;font-size:13px;color:var(--muted)}
.code{flex:1;min-height:0;overflow:auto;display:grid;grid-template-columns:auto 1fr;align-content:start}
.code pre{margin:0;padding:12px 0;font-size:13px;line-height:1.65}
#gutter{padding:12px 10px 12px 14px;text-align:right;color:var(--muted);opacity:.7;position:sticky;left:0;background:var(--pane);user-select:none}
#code{padding-left:10px;padding-right:24px}
.mk-err{background:var(--errbg);text-decoration:underline wavy var(--err);text-underline-offset:3px}
.mk-warn{text-decoration:underline wavy var(--warn);text-underline-offset:3px}
.hl{background:var(--hl);border-radius:2px;box-shadow:0 0 0 1px var(--accent)}
.eofmark{margin-left:4px;padding:0 6px;border-radius:9px;background:var(--err);color:#fff;font:11px system-ui,sans-serif}
.tabs{display:flex;gap:2px;padding:0 10px;border-bottom:1px solid var(--line);background:var(--pane);overflow-x:auto}
.tabs button{background:none;border:0;border-bottom:2px solid transparent;padding:9px 12px;color:var(--muted);cursor:pointer;white-space:nowrap}
.tabs button:hover{color:var(--ink)}
.tabs button[aria-selected=true]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
.badge{margin-left:6px;padding:0 6px;border-radius:9px;background:var(--err);color:#fff;font-size:11px;font-weight:600}
.badge.warn{background:var(--warn)}
.panel{flex:1;min-height:0;display:flex;flex-direction:column}
.scroll{flex:1;min-height:0;overflow:auto;padding:14px 18px}
.bar{display:flex;gap:8px;align-items:center;margin-bottom:10px}
.note{margin:0;padding:18px;color:var(--muted)}
#dgm{position:relative;flex:1;min-height:200px;overflow:hidden;cursor:grab;touch-action:none;
background-color:var(--page);background-image:radial-gradient(var(--dot) 1px,transparent 1px);background-size:22px 22px}
#dgm.drag{cursor:grabbing}
#dgm.empty{cursor:default}
#stage{position:absolute;left:0;top:0;transform-origin:0 0}
#stage svg{display:block;overflow:visible}
#stage .al-node{cursor:pointer}
.tools{position:absolute;top:10px;right:10px;display:flex;gap:6px}
.tools .btn{min-width:32px}
#dnote{position:absolute;left:0;right:0;top:0;padding:24px;color:var(--muted)}
pre.out{margin:0;font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
table{border-collapse:collapse;width:100%;font-size:13px}
th{position:sticky;top:-14px;background:var(--page);text-align:left;font-weight:600;color:var(--muted);padding:6px 10px;border-bottom:1px solid var(--line)}
td{padding:4px 10px;border-bottom:1px solid var(--line);vertical-align:top}
tbody tr:hover{background:var(--hl)}
tr.side td{opacity:.55}
td.num{text-align:right;color:var(--muted)}
ul.probs{list-style:none;margin:0;padding:0}
.prob{display:grid;grid-template-columns:64px 64px minmax(0,1fr) auto;gap:10px;padding:9px 18px;border-bottom:1px solid var(--line);cursor:pointer}
.prob:hover,.prob[aria-current=true]{background:var(--hl)}
.prob .sev{font-weight:600;color:var(--err)}
.prob.warning .sev{color:var(--warn)}
.prob .loc{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--muted)}
.prob .src{color:var(--muted);font-size:12.5px}
.gram h3{margin:0 0 6px;font-size:13px;color:var(--muted);font-weight:600}
.gram{margin-bottom:22px}
@media (max-width:900px){
body{display:block;height:auto}
main{display:block}
.left{border-right:0;border-bottom:1px solid var(--line);max-height:42vh}
.right{height:88vh}
.vdetail{white-space:normal}
}
@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
)CSS";
s += svgStyle(true);
return s;
}
// ------------------------------------------------------------------ HTML ----
inline std::string shellHead() {
return R"HTML(<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ANTLR Lab</title>
<script>try{var t=localStorage.getItem('antlr-lab-theme');if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t)}catch(e){}</script>
<style>
)HTML";
}
inline std::string shellBody() {
return R"HTML(
</style>
</head>
<body>
<header class="top">
<span class="brand">ANTLR Lab</span>
<div class="files" id="files"></div>
<span class="spacer"></span>
<span class="live" id="live" hidden>Auto-refresh is on</span>
<button class="btn" id="theme" type="button">Switch theme</button>
</header>
<section class="verdict" id="verdict" aria-live="polite">
<h1 class="vhead" id="vhead"></h1>
<p class="vdetail" id="vdetail"></p>
<div class="vmeta" id="vmeta"></div>
</section>
<main>
<section class="left" aria-label="Input">
<div class="ptitle">Input</div>
<div class="code"><pre id="gutter" aria-hidden="true"></pre><pre id="code"></pre></div>
</section>
<section class="right">
<div class="tabs" role="tablist" aria-label="Results">
<button role="tab" id="t-diagram" type="button">Diagram</button>
<button role="tab" id="t-tree" type="button">Tree</button>
<button role="tab" id="t-tokens" type="button">Tokens</button>
<button role="tab" id="t-problems" type="button">Problems<span class="badge" id="badge" hidden></span></button>
<button role="tab" id="t-grammar" type="button">Grammar</button>
</div>
<div class="panel" id="p-diagram" role="tabpanel">
<div id="dgm" tabindex="0" aria-label="Parse tree diagram. Drag to pan, scroll to zoom.">
<div id="stage"></div>
<p id="dnote" hidden></p>
<div class="tools">
<button class="btn" id="zin" type="button" aria-label="Zoom in">+</button>
<button class="btn" id="zout" type="button" aria-label="Zoom out">&minus;</button>
<button class="btn" id="zfit" type="button">Fit</button>
<button class="btn" id="psvg" type="button">Save SVG</button>
<button class="btn" id="ppng" type="button">Save PNG</button>
</div>
</div>
</div>
<div class="panel" id="p-tree" role="tabpanel" hidden>
<div class="scroll">
<div class="bar">
<button class="btn" id="b-pretty" type="button">Indented</button>
<button class="btn" id="b-flat" type="button">One line</button>
<button class="btn" id="b-copy" type="button">Copy</button>
</div>
<pre class="out" id="treeout"></pre>
</div>
</div>
<div class="panel" id="p-tokens" role="tabpanel" hidden>
<div class="scroll" id="tokwrap"></div>
</div>
<div class="panel" id="p-problems" role="tabpanel" hidden>
<div class="scroll" style="padding:0">
<p class="note" id="noprob">No problems found.</p>
<ul class="probs" id="problist"></ul>
</div>
</div>
<div class="panel" id="p-grammar" role="tabpanel" hidden>
<div class="scroll" id="gramwrap"></div>
</div>
</section>
</main>
)HTML";
}
// ------------------------------------------------------------ JavaScript ----
inline std::string jsCore() {
return R"JS(
(function () {
'use strict';
var D = window.LAB_DATA || {};
var cps = [];
var eofError = false;
var hover = null, pin = null;
var seen = -1;
var view = { tab: null, pretty: true, z: 1, x: 0, y: 0, w: 0, h: 0, needFit: true, svg: '' };
var TABS = ['diagram', 'tree', 'tokens', 'problems', 'grammar'];
var MAX_ROWS = 5000;
function $(id) { return document.getElementById(id); }
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text !== undefined && text !== null) e.textContent = text;
return e;
}
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
function plural(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); }
/* ---------- verdict ---------- */
function paintVerdict() {
var st = D.status || { kind: 'ok', text: '', detail: '' };
$('verdict').className = 'verdict ' + st.kind;
$('vhead').textContent = st.text;
$('vdetail').textContent = st.detail || '';
$('vdetail').title = st.detail || '';
var m = $('vmeta');
clear(m);
(D.meta || []).forEach(function (t) { m.appendChild(el('span', 'meta', t)); });
document.title = st.text + ' \u2013 ANTLR Lab';
var f = $('files');
clear(f);
(D.grammars || []).forEach(function (g) { f.appendChild(el('span', '', g.name)); });
if (D.startRule) f.appendChild(el('span', '', 'start rule: ' + D.startRule));
}
/* ---------- input pane ---------- */
function flagClass(f) {
var c = [];
if (f & 1) c.push('mk-err');
if (f & 2) c.push('mk-warn');
if (f & 4) c.push('hl');
return c.join(' ');
}
function paintInput() {
var box = $('code');
var n = cps.length;
var flags = new Uint8Array(n + 1);
(D.diagnostics || []).forEach(function (d) {
if (d.s < 0) return;
var bit = d.sev === 'warning' ? 2 : 1;
for (var a = d.s; a <= d.e && a < n; a++) flags[a] |= bit;
});
var r = hover || pin;
if (r) for (var b = r[0]; b <= r[1] && b < n; b++) flags[b] |= 4;
clear(box);
var frag = document.createDocumentFragment();
var i = 0;
while (i < n) {
var f = flags[i], j = i + 1;
while (j < n && flags[j] === f) j++;
var txt = cps.slice(i, j).join('');
frag.appendChild(f === 0 ? document.createTextNode(txt) : el('span', flagClass(f), txt));
i = j;
}
if (n === 0) frag.appendChild(el('span', 'note', '(empty input)'));
if (eofError) frag.appendChild(el('span', 'eofmark', 'end of input'));
box.appendChild(frag);
}
function paintGutter() {
var lines = 1;
for (var i = 0; i < cps.length; i++) if (cps[i] === '\n') lines++;
var nums = [];
for (var k = 1; k <= lines; k++) nums.push(k);
$('gutter').textContent = nums.join('\n');
}
var queued = false;
function repaintSoon() {
if (queued) return;
queued = true;
requestAnimationFrame(function () { queued = false; paintInput(); });
}
function setHover(s, e) {
hover = (s >= 0 && e >= s) ? [s, e] : null;
repaintSoon();
}
function setPin(s, e) {
pin = (s >= 0 && e >= s) ? [s, e] : null;
paintInput();
var h = document.querySelector('#code .hl');
if (h && h.scrollIntoView) h.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
/* ---------- tabs ---------- */
function showTab(t) {
view.tab = t;
TABS.forEach(function (id) {
var on = id === t;
var b = $('t-' + id);
b.setAttribute('aria-selected', on ? 'true' : 'false');
b.tabIndex = on ? 0 : -1;
$('p-' + id).hidden = !on;
});
if (t === 'diagram' && view.needFit) fit();
}
function initTabs() {
TABS.forEach(function (id, idx) {
var b = $('t-' + id);
b.onclick = function () { showTab(id); };
b.onkeydown = function (ev) {
var d = ev.key === 'ArrowRight' ? 1 : ev.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
var next = TABS[(idx + d + TABS.length) % TABS.length];
showTab(next);
$('t-' + next).focus();
ev.preventDefault();
};
});
}
/* ---------- tree text ---------- */
function paintTree() {
$('treeout').textContent = D.tree ? (view.pretty ? D.treePretty : D.tree) : (D.noDiagram || 'No parse tree.');
$('b-pretty').setAttribute('aria-pressed', view.pretty ? 'true' : 'false');
$('b-flat').setAttribute('aria-pressed', view.pretty ? 'false' : 'true');
}
function copyText(text, btn) {
var done = function () {
var old = btn.textContent;
btn.textContent = 'Copied';
setTimeout(function () { btn.textContent = old; }, 1200);
};
var fallback = function () {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); done(); } catch (e) { /* ignore */ }
ta.remove();
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(done, fallback);
} else {
fallback();
}
}
/* ---------- tokens ---------- */
function paintTokens() {
var wrap = $('tokwrap');
clear(wrap);
var toks = D.tokens || [];
if (!toks.length) { wrap.appendChild(el('p', 'note', 'No tokens.')); return; }
var table = el('table');
var head = el('tr');
['#', 'Text', 'Type', 'Channel', 'Line:col'].forEach(function (h) { head.appendChild(el('th', '', h)); });
var thead = el('thead');
thead.appendChild(head);
table.appendChild(thead);
var body = el('tbody');
toks.slice(0, MAX_ROWS).forEach(function (t) {
var tr = el('tr', t.ch && t.ch !== 'DEFAULT_TOKEN_CHANNEL' && t.ch !== '0' ? 'side' : '');
tr.appendChild(el('td', 'num', String(t.i)));
tr.appendChild(el('td', 'mono', t.t));
tr.appendChild(el('td', 'mono', t.ty));
tr.appendChild(el('td', 'mono', t.ch));
tr.appendChild(el('td', 'mono', t.l + ':' + t.c));
tr.onmouseenter = function () { setHover(t.s, t.e); };
tr.onmouseleave = function () { setHover(-1, -1); };
body.appendChild(tr);
});
table.appendChild(body);
wrap.appendChild(table);
if (toks.length > MAX_ROWS) {
wrap.appendChild(el('p', 'note', 'Showing the first ' + MAX_ROWS + ' of ' + toks.length + ' tokens. tokens.txt has all of them.'));
}
}
/* ---------- problems ---------- */
function paintProblems() {
var list = $('problist');
clear(list);
var ds = D.diagnostics || [];
$('noprob').hidden = ds.length > 0;
var errs = 0, warns = 0;
ds.forEach(function (d) {
if (d.sev === 'error') errs++; else warns++;
var li = el('li', 'prob ' + d.sev);
li.tabIndex = 0;
li.appendChild(el('span', 'sev', d.sev));
li.appendChild(el('span', 'loc', d.line ? d.line + ':' + d.col : ''));
li.appendChild(el('span', 'msg', d.msg));
li.appendChild(el('span', 'src', d.src));
var act = function () {
Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-current'); });
li.setAttribute('aria-current', 'true');
setPin(d.s, d.e);
};
li.onclick = act;
li.onkeydown = function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { act(); ev.preventDefault(); } };
list.appendChild(li);
});
var badge = $('badge');
badge.hidden = !ds.length;
badge.className = 'badge' + (errs ? '' : ' warn');
badge.textContent = String(errs || warns);
}
/* ---------- grammar ---------- */
function paintGrammar() {
var wrap = $('gramwrap');
clear(wrap);
if (D.toolLog) {
var s0 = el('section', 'gram');
s0.appendChild(el('h3', '', 'ANTLR tool output'));
s0.appendChild(el('pre', 'out', D.toolLog));
wrap.appendChild(s0);
}
(D.grammars || []).forEach(function (g) {
var s = el('section', 'gram');
s.appendChild(el('h3', '', g.name));
s.appendChild(el('pre', 'out', g.text));
wrap.appendChild(s);
});
}
)JS";
}
inline std::string jsDiagram() {
return R"JS(
/* ---------- diagram ---------- */
function applyView() {
$('stage').style.transform = 'translate(' + view.x + 'px,' + view.y + 'px) scale(' + view.z + ')';
}
function fit() {
var c = $('dgm');
var cw = c.clientWidth, ch = c.clientHeight;
if (!view.w || !cw || !ch) return;
var z = Math.min(1, (cw - 40) / view.w, (ch - 40) / view.h);
z = Math.max(z, 0.05);
view.z = z;
view.x = (cw - view.w * z) / 2;
view.y = Math.max(20, (ch - view.h * z) / 2);
view.needFit = false;
applyView();
}
function zoomAt(cx, cy, factor) {
var z = Math.min(8, Math.max(0.05, view.z * factor));
var k = z / view.z;
view.x = cx - (cx - view.x) * k;
view.y = cy - (cy - view.y) * k;
view.z = z;
applyView();
}
function zoomCentre(factor) {
var c = $('dgm');
zoomAt(c.clientWidth / 2, c.clientHeight / 2, factor);
}
function paintDiagram() {
var stage = $('stage'), note = $('dnote'), box = $('dgm');
var had = !!view.svg;
if (D.svg === view.svg && had) return; // same picture: keep pan and zoom
view.svg = D.svg || '';
clear(stage);
if (!view.svg) {
note.textContent = D.noDiagram || 'No diagram.';
note.hidden = false;
box.classList.add('empty');
view.w = view.h = 0;
view.needFit = true;
return;
}
note.hidden = true;
box.classList.remove('empty');
stage.innerHTML = view.svg;
var vb = stage.querySelector('svg').getAttribute('viewBox').split(' ');
view.w = parseFloat(vb[2]);
view.h = parseFloat(vb[3]);
if (!had) view.needFit = true;
if (view.tab === 'diagram' && view.needFit) fit(); else applyView();
}
function initDiagram() {
var box = $('dgm'), stage = $('stage');
var drag = null, moved = false;
box.addEventListener('wheel', function (ev) {
if (!view.w) return;
ev.preventDefault();
var unit = ev.deltaMode === 1 ? 16 : 1;
var r = box.getBoundingClientRect();
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(-ev.deltaY * unit * (ev.ctrlKey ? 0.01 : 0.0015)));
}, { passive: false });
box.addEventListener('pointerdown', function (ev) {
if (ev.button !== 0 || ev.target.closest('.tools') || !view.w) return;
drag = { x: ev.clientX, y: ev.clientY, vx: view.x, vy: view.y };
moved = false;
box.setPointerCapture(ev.pointerId);
box.classList.add('drag');
});
box.addEventListener('pointermove', function (ev) {
if (!drag) return;
var dx = ev.clientX - drag.x, dy = ev.clientY - drag.y;
if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
view.x = drag.vx + dx;
view.y = drag.vy + dy;
applyView();
});
var end = function () { drag = null; box.classList.remove('drag'); };
box.addEventListener('pointerup', end);
box.addEventListener('pointercancel', end);
box.addEventListener('dblclick', function (ev) { if (!ev.target.closest('.tools')) fit(); });
stage.addEventListener('click', function (ev) {
if (moved) return;
var g = ev.target.closest('.al-node');
if (!g || !g.hasAttribute('data-s')) return;
var s = +g.getAttribute('data-s'), e = +g.getAttribute('data-e');
if (pin && pin[0] === s && pin[1] === e) setPin(-1, -1); else setPin(s, e);
});
stage.addEventListener('mouseover', function (ev) {
var g = ev.target.closest('.al-node');
if (g && g.hasAttribute('data-s')) setHover(+g.getAttribute('data-s'), +g.getAttribute('data-e'));
});
stage.addEventListener('mouseout', function (ev) {
if (ev.target.closest('.al-node')) setHover(-1, -1);
});
box.addEventListener('keydown', function (ev) {
var step = 60;
if (ev.key === '+' || ev.key === '=') zoomCentre(1.25);
else if (ev.key === '-') zoomCentre(0.8);
else if (ev.key === '0') fit();
else if (ev.key === 'ArrowLeft') { view.x += step; applyView(); }
else if (ev.key === 'ArrowRight') { view.x -= step; applyView(); }
else if (ev.key === 'ArrowUp') { view.y += step; applyView(); }
else if (ev.key === 'ArrowDown') { view.y -= step; applyView(); }
else return;
ev.preventDefault();
});
$('zin').onclick = function () { zoomCentre(1.25); };
$('zout').onclick = function () { zoomCentre(0.8); };
$('zfit').onclick = fit;
$('psvg').onclick = function () { if (view.w) download('tree.svg', new Blob([svgMarkup()], { type: 'image/svg+xml' })); };
$('ppng').onclick = savePng;
window.addEventListener('resize', function () { if (view.tab === 'diagram' && view.needFit) fit(); });
}
/* ---------- export ---------- */
function svgMarkup() {
var NS = 'http://www.w3.org/2000/svg';
var c = $('stage').querySelector('svg').cloneNode(true);
var st = document.createElementNS(NS, 'style');
st.textContent = D.svgStyle;
var bg = document.createElementNS(NS, 'rect');
bg.setAttribute('class', 'al-bg');
bg.setAttribute('width', '100%');
bg.setAttribute('height', '100%');
c.insertBefore(bg, c.firstChild);
c.insertBefore(st, c.firstChild);
return '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(c);
}
function download(name, blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
}
function savePng() {
if (!view.w) return;
var scale = Math.min(2, 8000 / Math.max(view.w, view.h));
var img = new Image();
img.onload = function () {
var c = document.createElement('canvas');
c.width = Math.max(1, Math.round(view.w * scale));
c.height = Math.max(1, Math.round(view.h * scale));
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
c.toBlob(function (b) { if (b) download('tree.png', b); }, 'image/png');
};
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup());
}
/* ---------- theme ---------- */
function effectiveTheme() {
var t = document.documentElement.getAttribute('data-theme');
if (t) return t;
return window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function paintThemeButton() {
$('theme').textContent = effectiveTheme() === 'dark' ? 'Switch to light' : 'Switch to dark';
}
function initTheme() {
$('theme').onclick = function () {
var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('antlr-lab-theme', next); } catch (e) { /* ignore */ }
paintThemeButton();
};
if (window.matchMedia) {
var mq = matchMedia('(prefers-color-scheme: dark)');
var on = function () { paintThemeButton(); };
if (mq.addEventListener) mq.addEventListener('change', on); else if (mq.addListener) mq.addListener(on);
}
paintThemeButton();
}
/* ---------- main ---------- */
function render() {
seen = D.version;
cps = Array.from(D.input || '');
eofError = (D.diagnostics || []).some(function (d) { return d.sev === 'error' && d.s < 0 && d.src === 'parser'; });
hover = null;
if (pin && pin[1] >= cps.length) pin = null;
paintVerdict();
paintGutter();
paintInput();
paintDiagram();
paintTree();
paintTokens();
paintProblems();
paintGrammar();
if (view.tab === null) {
var kind = (D.status || {}).kind;
showTab(kind === 'fail' ? 'grammar' : (D.svg ? 'diagram' : (D.tree ? 'tree' : 'tokens')));
} else if (view.tab === 'diagram' && view.needFit) {
fit();
}
}
window.labUpdate = function (data) {
if (!data || data.version === seen) return;
D = data;
window.LAB_DATA = data;
render();
};
initTabs();
initDiagram();
initTheme();
$('b-pretty').onclick = function () { view.pretty = true; paintTree(); };
$('b-flat').onclick = function () { view.pretty = false; paintTree(); };
$('b-copy').onclick = function () { copyText(view.pretty ? D.treePretty : D.tree, $('b-copy')); };
render();
if (D.live) {
$('live').hidden = false;
setInterval(function () {
var old = document.getElementById('poll');
if (old) old.remove();
var s = document.createElement('script');
s.id = 'poll';
s.src = 'report-data.js?t=' + Date.now();
document.head.appendChild(s);
}, 1000);
}
})();
)JS";
}
} // namespace web
// ------------------------------------------------------------ the page ----
inline std::string reportHtml(const ReportInput& in) {
std::string h;
h += web::shellHead();
h += web::css();
h += web::shellBody();
h += "<script>window.LAB_DATA=" + reportJson(in) + ";</script>\n<script>";
h += web::jsCore();
h += web::jsDiagram();
h += "</script>\n</body>\n</html>\n";
return h;
}
} // namespace lab
+1047
View File
File diff suppressed because it is too large. Load diff
+91
View File
@@ -0,0 +1,91 @@
// Plain data types shared by the analysis, rendering and reporting code.
// Nothing in here depends on the ANTLR runtime.
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace lab {
// One node of the parse tree, copied out of the ANTLR tree so that rendering
// does not need the runtime (and stays valid after the parser is destroyed).
struct Node {
enum class Kind { Rule, Terminal, Error };
Kind kind = Kind::Rule;
std::string label; // rule name, or the raw token text
std::string typeName; // token type name (terminals and error nodes only)
int64_t start = -1; // input range as code point indices, inclusive; -1 = none
int64_t stop = -1;
size_t line = 0; // position of a token (1-based line, 0-based column)
size_t col = 0;
std::vector<Node> children;
Node() = default;
Node(const Node&) = default;
Node(Node&&) noexcept = default;
Node& operator=(const Node&) = default;
Node& operator=(Node&&) noexcept = default;
// The default destructor would recurse once per tree level; a parse tree
// that is thousands of levels deep (a long right-recursive list) could
// overflow the stack just by being freed. Take the tree apart iteratively.
~Node() {
if (children.empty()) return;
std::vector<std::vector<Node>> pending;
pending.push_back(std::move(children));
while (!pending.empty()) {
std::vector<Node> level = std::move(pending.back());
pending.pop_back();
for (Node& c : level) {
if (!c.children.empty()) pending.push_back(std::move(c.children));
}
} // each `level` dies here with childless nodes, so nothing recurses
}
};
struct TokenInfo {
size_t index = 0;
std::string text; // raw text
std::string typeName;
std::string channel;
size_t line = 0;
size_t col = 0;
int64_t start = -1; // code point indices, inclusive; -1 = none (EOF)
int64_t stop = -1;
bool isEof = false;
};
struct Diagnostic {
enum class Severity { Error, Warning };
Severity severity = Severity::Error;
std::string source; // "lexer", "parser" or "input"
size_t line = 0;
size_t col = 0;
int64_t start = -1; // range to underline in the input, -1 = none
int64_t stop = -1;
std::string message;
};
struct AnalysisResult {
std::vector<TokenInfo> tokens;
std::vector<Diagnostic> diagnostics;
bool hasTree = false;
Node tree;
std::string startRule;
size_t nodeCount = 0;
double lexMs = 0;
double parseMs = 0;
size_t errorCount() const {
size_t n = 0;
for (const auto& d : diagnostics) n += d.severity == Diagnostic::Severity::Error;
return n;
}
size_t warningCount() const { return diagnostics.size() - errorCount(); }
};
} // namespace lab
+239
View File
@@ -0,0 +1,239 @@
// Small helpers: files, UTF-8, escaping.
//
// NOTE: keep the standard headers used by the rest of the program in this file
// (or included before it). The ANTLR runtime #undef's the EOF macro from
// <cstdio>, and that only sticks if <cstdio> was already included. platform.hpp
// (included here) takes care of that and of windows.h.
#pragma once
#include "platform.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
#include <vector>
namespace lab {
// ---------------------------------------------------------------- files ----
// Paths given as std::string are UTF-8 (see platform.hpp); the fs::path
// overloads are for paths that already are paths.
inline bool readFile(const fs::path& p, std::string& out);
inline bool readFile(const std::string& utf8Path, std::string& out) {
return readFile(pathFromUtf8(utf8Path), out);
}
inline bool readFile(const fs::path& p, std::string& out) {
std::ifstream in(p, std::ios::binary);
if (!in) return false;
std::ostringstream ss;
ss << in.rdbuf();
out = ss.str();
return true;
}
inline bool writeFile(const fs::path& p, const std::string& data) {
std::ofstream out(p, std::ios::binary | std::ios::trunc);
if (!out) return false;
out.write(data.data(), static_cast<std::streamsize>(data.size()));
return static_cast<bool>(out);
}
// Write to a temp file and rename, so a browser polling the file never sees a
// half-written version.
inline bool writeFileAtomic(const fs::path& p, const std::string& data) {
fs::path tmp = p;
tmp += ".tmp";
if (!writeFile(tmp, data)) return false;
std::error_code ec;
fs::rename(tmp, p, ec);
if (ec) {
fs::remove(tmp, ec);
return writeFile(p, data);
}
return true;
}
// ---------------------------------------------------------------- UTF-8 ----
// Replaces invalid UTF-8 with U+FFFD and drops a leading BOM. The sanitized
// text is what gets fed to ANTLR *and* shown in the report, so code point
// indices agree everywhere.
inline std::string sanitizeUtf8(const std::string& in) {
std::string out;
out.reserve(in.size());
const size_t n = in.size();
size_t i = 0;
auto byteAt = [&](size_t k) { return static_cast<uint8_t>(in[k]); };
if (n >= 3 && byteAt(0) == 0xEF && byteAt(1) == 0xBB && byteAt(2) == 0xBF) i = 3;
auto cont = [&](size_t k) { return k < n && (byteAt(k) & 0xC0) == 0x80; };
while (i < n) {
const uint8_t c = byteAt(i);
size_t len = 0;
if (c < 0x80) {
len = 1;
} else if (c >= 0xC2 && c <= 0xDF) {
len = cont(i + 1) ? 2 : 0;
} else if (c >= 0xE0 && c <= 0xEF) {
if (cont(i + 1) && cont(i + 2)) {
const uint8_t c1 = byteAt(i + 1);
len = ((c == 0xE0 && c1 < 0xA0) || (c == 0xED && c1 > 0x9F)) ? 0 : 3;
}
} else if (c >= 0xF0 && c <= 0xF4) {
if (cont(i + 1) && cont(i + 2) && cont(i + 3)) {
const uint8_t c1 = byteAt(i + 1);
len = ((c == 0xF0 && c1 < 0x90) || (c == 0xF4 && c1 > 0x8F)) ? 0 : 4;
}
}
if (len == 0) {
out += "\xEF\xBF\xBD";
++i;
} else {
out.append(in, i, len);
i += len;
}
}
return out;
}
inline size_t cpCount(const std::string& s) {
size_t n = 0;
for (unsigned char c : s) n += (c & 0xC0) != 0x80;
return n;
}
// Shorten to at most maxCp code points, ending with an ellipsis.
inline std::string truncateCp(const std::string& s, size_t maxCp) {
if (maxCp < 2 || cpCount(s) <= maxCp) return s;
size_t seen = 0, i = 0;
while (i < s.size()) {
if ((static_cast<unsigned char>(s[i]) & 0xC0) != 0x80) {
if (seen == maxCp - 1) break;
++seen;
}
++i;
}
return s.substr(0, i) + "\xE2\x80\xA6";
}
// Maps ANTLR (line, column) positions to code point indices in the input.
class LineTable {
public:
explicit LineTable(const std::string& utf8) {
starts_.push_back(0);
int64_t cp = 0;
for (unsigned char c : utf8) {
if ((c & 0xC0) == 0x80) continue;
++cp;
if (c == '\n') starts_.push_back(cp);
}
total_ = cp;
}
int64_t total() const { return total_; }
// line is 1-based, col is 0-based. Returns -1 when the position is outside
// the input (for example at EOF).
int64_t indexOf(size_t line, size_t col) const {
if (line == 0 || line > starts_.size()) return -1;
const int64_t idx = starts_[line - 1] + static_cast<int64_t>(col);
return idx < total_ ? idx : -1;
}
private:
std::vector<int64_t> starts_;
int64_t total_ = 0;
};
// ------------------------------------------------------------- escaping ----
// Make control characters visible: "\n" -> \n, other controls -> \xNN.
inline std::string visible(const std::string& s) {
std::string o;
o.reserve(s.size());
for (unsigned char c : s) {
switch (c) {
case '\n': o += "\\n"; break;
case '\r': o += "\\r"; break;
case '\t': o += "\\t"; break;
default:
if (c < 0x20 || c == 0x7F) {
char b[8];
std::snprintf(b, sizeof b, "\\x%02X", c);
o += b;
} else {
o += static_cast<char>(c);
}
}
}
return o;
}
inline std::string escapeXml(const std::string& s) {
std::string o;
o.reserve(s.size() + 8);
for (char c : s) {
switch (c) {
case '&': o += "&amp;"; break;
case '<': o += "&lt;"; break;
case '>': o += "&gt;"; break;
case '"': o += "&quot;"; break;
case '\'': o += "&#39;"; break;
default: o += c;
}
}
return o;
}
// JSON string literal that is also safe to embed inside a <script> element.
inline std::string jsonString(const std::string& s) {
std::string o;
o.reserve(s.size() + 2);
o += '"';
for (size_t i = 0; i < s.size(); ++i) {
const unsigned char c = static_cast<unsigned char>(s[i]);
switch (c) {
case '"': o += "\\\""; break;
case '\\': o += "\\\\"; break;
case '\n': o += "\\n"; break;
case '\r': o += "\\r"; break;
case '\t': o += "\\t"; break;
case '<': o += "\\u003c"; break;
case '>': o += "\\u003e"; break;
case '&': o += "\\u0026"; break;
default:
if (c < 0x20 || c == 0x7F) {
char b[8];
std::snprintf(b, sizeof b, "\\u%04x", c);
o += b;
} else if (c == 0xE2 && i + 2 < s.size() &&
static_cast<unsigned char>(s[i + 1]) == 0x80 &&
(static_cast<unsigned char>(s[i + 2]) & 0xFE) == 0xA8) {
o += (static_cast<unsigned char>(s[i + 2]) == 0xA8) ? "\\u2028" : "\\u2029";
i += 2;
} else {
o += static_cast<char>(c);
}
}
}
o += '"';
return o;
}
inline std::string plural(size_t n, const std::string& word) {
return std::to_string(n) + " " + word + (n == 1 ? "" : "s");
}
inline std::string fixed1(double v) {
char b[48];
std::snprintf(b, sizeof b, "%.1f", v);
return b;
}
} // namespace lab
+2155
View File
File diff suppressed because it is too large. Load diff
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Embeds web/ (the browser UI) into src/webui_assets.hpp.
Run this after editing anything in web/: python3 tools/embed_web.py
Each file becomes a few C++ raw string literals of at most ~12 KB, because
MSVC rejects longer string literals. The generated header is checked in, so
building antlrlab does not need Python.
"""
import glob
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WEB = os.path.join(ROOT, "web")
OUT = os.path.join(ROOT, "src", "webui_assets.hpp")
DELIM = "__WEB__"
CHUNK = 12000
def read(path):
with open(path, encoding="utf-8") as f:
text = f.read()
try:
text.encode("ascii")
except UnicodeEncodeError as e:
sys.exit("%s contains non-ASCII text (%s). Use \\u escapes or entities." % (path, e))
if ")" + DELIM + '"' in text:
sys.exit("%s contains the raw string terminator" % path)
return text
def chunks(text):
out, cur = [], ""
for line in text.splitlines(keepends=True):
if len(cur) + len(line) > CHUNK and cur:
out.append(cur)
cur = ""
cur += line
if cur:
out.append(cur)
return out
def function(name, text):
body = "".join(' s += R"%s(%s)%s";\n' % (DELIM, c, DELIM) for c in chunks(text))
return "inline std::string %s() {\n std::string s;\n%s return s;\n}\n" % (name, body)
def main():
js = "".join(read(p) for p in sorted(glob.glob(os.path.join(WEB, "[0-9]*.js"))))
parts = [
"// GENERATED by tools/embed_web.py from web/ - do not edit by hand.\n",
"#pragma once\n\n#include <string>\n\nnamespace lab {\n\n",
function("webIndexHtml", read(os.path.join(WEB, "index.html"))),
"\n",
function("webCss", read(os.path.join(WEB, "app.css"))),
"\n",
function("webJs", js),
"\n} // namespace lab\n",
]
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
f.write("".join(parts))
print("wrote %s (%d bytes of JS, %d files)" % (OUT, len(js), len(glob.glob(os.path.join(WEB, "[0-9]*.js")))))
main()
+226
View File
@@ -0,0 +1,226 @@
(function () {
'use strict';
/* ================================================================== helpers */
var $ = function (id) { return document.getElementById(id); };
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text !== undefined && text !== null) e.textContent = text;
return e;
}
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
function plural(n, w) { return n + ' ' + w + (n === 1 ? '' : 's'); }
function basename(p) { var i = p.lastIndexOf('/'); return i < 0 ? p : p.slice(i + 1); }
function dirname(p) { var i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i); }
function joinPath(d, n) { return d ? d + '/' + n : n; }
function stem(n) { var i = n.lastIndexOf('.'); return i <= 0 ? n : n.slice(0, i); }
function isGrammarFile(p) { return /\.g4$/i.test(p); }
function human(n) { return n < 1024 ? n + ' B' : n < 1048576 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(1) + ' MB'; }
var store = {
get: function (k, d) {
try { var v = localStorage.getItem('antlrlab:' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; }
},
set: function (k, v) { try { localStorage.setItem('antlrlab:' + k, JSON.stringify(v)); } catch (e) { /* ignore */ } }
};
function debounce(fn, ms) {
var t = null;
var d = function () { clearTimeout(t); var a = arguments; t = setTimeout(function () { fn.apply(null, a); }, ms); };
d.cancel = function () { clearTimeout(t); };
return d;
}
/* ==================================================================== state */
var S = {
info: null,
tabs: [],
active: null, // id of the visible tab
lastGrammar: null, // the grammar that Run uses
lastInput: null, // the input that Run uses
results: {}, // input tab id -> { data, rev, grev }
gres: null, // last grammar outcome from the server
crash: null, // set when the last run crashed, timed out or failed in the worker
grev: 0, // bumps on every grammar edit
start: {}, // grammar name -> chosen start rule
prefs: { autorun: true, theme: 'auto', fs: 13, diagram: true, output: true, view: 'diagram', out: 'problems' },
running: false,
queued: false,
untitled: { grammar: 0, input: 0 }
};
/* ====================================================================== API */
function api(method, path, body) {
var opts = { method: method, credentials: 'same-origin', headers: { 'X-Requested-With': 'antlrlab' } };
if (body !== undefined) {
opts.body = JSON.stringify(body);
opts.headers['Content-Type'] = 'application/json';
}
return fetch(path, opts).then(function (r) {
return r.text().then(function (t) {
var data = null;
try { data = JSON.parse(t); } catch (e) { /* not JSON */ }
if (!r.ok) {
var err = new Error((data && data.error) || ('HTTP ' + r.status));
err.status = r.status;
err.data = data;
throw err;
}
return data;
});
});
}
/* ==================================================================== toast */
function toast(msg, kind) {
var t = el('div', 'toast' + (kind ? ' ' + kind : ''), msg);
$('toasts').appendChild(t);
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, kind === 'error' ? 6500 : 2600);
}
/* ================================================================== dialogs */
// modal({title, build(ctx) -> Node, buttons: [{label, value, primary}]}) -> Promise(value | null)
function modal(opts) {
return new Promise(function (resolve) {
var root = $('modal-root');
var prev = document.activeElement;
clear(root);
var back = el('div', 'modal-back');
var box = el('div', 'modal');
box.setAttribute('role', 'dialog');
box.setAttribute('aria-modal', 'true');
box.appendChild(el('h3', '', opts.title));
var body = el('div', 'mbody');
var ctx = { close: close, box: box, accept: null };
if (opts.message) body.appendChild(el('p', '', opts.message));
if (opts.build) body.appendChild(opts.build(ctx));
box.appendChild(body);
var foot = el('div', 'mfoot');
var primary = null;
(opts.buttons || []).forEach(function (b) {
var btn = el('button', 'btn' + (b.primary ? ' primary' : ''), b.label);
btn.type = 'button';
btn.onclick = function () {
if (b.check && !b.check()) return;
close(b.value === undefined ? b.label : b.value);
};
if (b.primary) primary = btn;
foot.appendChild(btn);
});
box.appendChild(foot);
back.appendChild(box);
root.appendChild(back);
function onKey(ev) {
if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); close(null); }
else if (ev.key === 'Enter' && primary && ev.target.tagName !== 'BUTTON' && !(ev.target.closest && ev.target.closest('.flist'))) {
ev.preventDefault();
ev.stopPropagation();
primary.click();
}
}
back.addEventListener('mousedown', function (ev) { if (ev.target === back) close(null); });
document.addEventListener('keydown', onKey, true);
function close(v) {
document.removeEventListener('keydown', onKey, true);
clear(root);
if (prev && prev.focus) prev.focus();
resolve(v);
}
var first = box.querySelector('[data-focus]') || primary || box.querySelector('button');
if (first) first.focus();
});
}
function ask(title, message, buttons) { return modal({ title: title, message: message, buttons: buttons }); }
/* ===================================================================== menus */
var menus = []; // [{label, items: function() -> [...]}]
var openMenu = -1;
var dropdown = null;
function defineMenus(list) {
menus = list;
var bar = $('menubar');
clear(bar);
menus.forEach(function (m, i) {
var b = el('button', 'menu-btn', m.label);
b.type = 'button';
b.setAttribute('role', 'menuitem');
b.setAttribute('aria-haspopup', 'true');
b.setAttribute('aria-expanded', 'false');
b.onclick = function () { if (openMenu === i) closeMenu(); else showMenu(i, false); };
b.onmouseenter = function () { if (openMenu >= 0 && openMenu !== i) showMenu(i, false); };
b.onkeydown = function (ev) {
if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); showMenu(i, true); }
else if (ev.key === 'ArrowRight') { ev.preventDefault(); bar.children[(i + 1) % menus.length].focus(); }
else if (ev.key === 'ArrowLeft') { ev.preventDefault(); bar.children[(i + menus.length - 1) % menus.length].focus(); }
};
bar.appendChild(b);
});
var rn = el('span', '', '');
rn.id = 'rootname';
bar.appendChild(rn);
}
function closeMenu(refocus) {
if (dropdown) { dropdown.remove(); dropdown = null; }
var bar = $('menubar');
Array.prototype.forEach.call(bar.querySelectorAll('.menu-btn'), function (b) { b.setAttribute('aria-expanded', 'false'); });
var was = openMenu;
openMenu = -1;
if (refocus && was >= 0) bar.children[was].focus();
}
function showMenu(i, focusFirst) {
closeMenu();
openMenu = i;
var btn = $('menubar').children[i];
btn.setAttribute('aria-expanded', 'true');
var dd = el('div', 'dropdown');
dd.setAttribute('role', 'menu');
menus[i].items().forEach(function (it) {
if (it.sep) { dd.appendChild(el('div', 'sep')); return; }
var row = el('div', 'mi');
row.setAttribute('role', 'menuitem');
row.tabIndex = -1;
if (it.disabled) row.setAttribute('aria-disabled', 'true');
if (it.checked) { row.appendChild(el('span', 'ck', '\u2713')); row.setAttribute('aria-checked', 'true'); }
row.appendChild(el('span', '', it.label));
if (it.key) row.appendChild(el('span', 'k', it.key));
var run = function () { closeMenu(); setTimeout(it.action, 0); };
row.onclick = run;
row.onkeydown = function (ev) {
var items = Array.prototype.filter.call(dd.children, function (c) { return c.classList.contains('mi') && c.getAttribute('aria-disabled') !== 'true'; });
var at = items.indexOf(row);
if (ev.key === 'ArrowDown') { ev.preventDefault(); items[(at + 1) % items.length].focus(); }
else if (ev.key === 'ArrowUp') { ev.preventDefault(); items[(at + items.length - 1) % items.length].focus(); }
else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); run(); }
else if (ev.key === 'Escape') { ev.preventDefault(); closeMenu(true); }
else if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
ev.preventDefault();
var n = (i + (ev.key === 'ArrowRight' ? 1 : menus.length - 1)) % menus.length;
showMenu(n, true);
$('menubar').children[n].focus();
var first = dropdown && dropdown.querySelector('.mi:not([aria-disabled=true])');
if (first) first.focus();
}
};
dd.appendChild(row);
});
document.body.appendChild(dd);
var r = btn.getBoundingClientRect();
dd.style.left = Math.max(0, Math.min(r.left, window.innerWidth - dd.offsetWidth - 4)) + 'px';
dd.style.top = r.bottom + 'px';
dropdown = dd;
if (focusFirst) {
var f = dd.querySelector('.mi:not([aria-disabled=true])');
if (f) f.focus();
}
}
document.addEventListener('mousedown', function (ev) {
if (dropdown && !dropdown.contains(ev.target) && !ev.target.closest('.menu-btn')) closeMenu();
});
window.addEventListener('blur', function () { closeMenu(); });
+476
View File
@@ -0,0 +1,476 @@
/* ================================================================== editor
* A plain <textarea> sits on top of a <pre> that paints the same text with
* syntax colors and marks. Both have identical font metrics and size, so the
* textarea provides the caret, selection, IME and native undo while the <pre>
* provides the colors. One editor (and so one undo history) per tab. */
var CLS = ['', 't-com', 't-str', 't-set', 't-act', 't-ann', 't-kw', 't-tok', 't-def', 't-lab', 't-op', 't-tokdef'];
var MKCLS = [[1, 'mk-err'], [2, 'mk-warn'], [4, 'mk-hl'], [8, 'mk-find'], [16, 'mk-cur']];
var KW = { grammar: 1, lexer: 1, parser: 1, fragment: 1, options: 1, tokens: 1, channels: 1, 'import': 1, mode: 1, returns: 1,
locals: 1, throws: 1, 'catch': 1, 'finally': 1, skip: 1, more: 1, pushMode: 1, popMode: 1, type: 1, channel: 1 };
function isIdStart(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'; }
function isIdChar(c) { return isIdStart(c) || (c >= '0' && c <= '9'); }
// One class id per UTF-16 unit, for ANTLR grammar syntax.
function g4Classes(t) {
var n = t.length, cls = new Uint8Array(n), i = 0, prevKw = '';
function paint(a, b, c) { for (var k = a; k < b; k++) cls[k] = c; }
function quoted(from, close) {
var j = from + 1;
while (j < n && t.charAt(j) !== '\n') {
if (t.charAt(j) === '\\') { j += 2; continue; }
if (t.charAt(j) === close) { j++; break; }
j++;
}
return Math.min(j, n);
}
while (i < n) {
var c = t.charAt(i), j;
if (c === '/' && t.charAt(i + 1) === '/') {
j = t.indexOf('\n', i); if (j < 0) j = n;
paint(i, j, 1); i = j;
} else if (c === '/' && t.charAt(i + 1) === '*') {
j = t.indexOf('*/', i + 2); j = j < 0 ? n : j + 2;
paint(i, j, 1); i = j;
} else if (c === "'") {
j = quoted(i, "'"); paint(i, j, 2); i = j;
} else if (c === '[') {
j = quoted(i, ']'); paint(i, j, 3); i = j;
} else if (c === '{') {
var depth = 0; j = i;
while (j < n) {
var d = t.charAt(j);
if (d === "'" || d === '"') { j = quoted(j, d); continue; }
if (d === '{') depth++;
else if (d === '}' && --depth === 0) { j++; break; }
j++;
}
paint(i, j, 4); i = j;
} else if (c === '@' || c === '#') {
j = i + 1;
while (j < n && (t.charAt(j) === ' ' || t.charAt(j) === '\t') && c === '#') j++;
while (j < n && (isIdChar(t.charAt(j)) || t.charAt(j) === ':')) j++;
paint(i, j, 5); i = j;
} else if (isIdStart(c)) {
j = i + 1;
while (j < n && isIdChar(t.charAt(j))) j++;
var word = t.slice(i, j);
var k = j;
while (k < n && (t.charAt(k) === ' ' || t.charAt(k) === '\t' || t.charAt(k) === '\n' || t.charAt(k) === '\r')) k++;
var next = t.charAt(k), after = t.charAt(k + 1);
var ls = t.lastIndexOf('\n', i - 1) + 1;
var first = /^[ \t]*$/.test(t.slice(ls, i));
var def = next === ':' && after !== ':' && (first || prevKw === 'fragment');
var c0 = word.charAt(0);
var up = c0 >= 'A' && c0 <= 'Z';
var id;
if (KW[word] === 1 && !def && next !== ':') id = 6;
else if (def) id = up ? 11 : 8;
else if ((next === '=' && after !== '=') || (next === '+' && after === '=')) id = 9;
else id = up ? 7 : 0;
paint(i, j, id);
prevKw = word;
i = j;
} else {
if ('|:;()*+?~.,<>=-'.indexOf(c) >= 0) cls[i] = 10;
i++;
}
}
return cls;
}
// code point index -> UTF-16 index
function cpMap(text) {
if (!/[\ud800-\udbff]/.test(text)) return function (i) { return i; };
var map = [];
for (var i = 0; i < text.length;) {
map.push(i);
var c = text.charCodeAt(i);
i += (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) ? 2 : 1;
}
map.push(text.length);
return function (k) { return k < map.length ? map[k] : text.length; };
}
/* ----------------------------------------------------------- line helpers */
function lineStarts(ed) {
if (!ed.ls) {
var t = ed.ta.value, a = [0];
for (var i = t.indexOf('\n'); i >= 0; i = t.indexOf('\n', i + 1)) a.push(i + 1);
ed.ls = a;
}
return ed.ls;
}
function lineOf(ed, idx) {
var a = lineStarts(ed), lo = 0, hi = a.length - 1;
while (lo < hi) {
var mid = (lo + hi + 1) >> 1;
if (a[mid] <= idx) lo = mid; else hi = mid - 1;
}
return lo;
}
// 1-based line + 0-based column -> unit index (column clamped to the line)
function unitAt(ed, line, col) {
var a = lineStarts(ed);
if (line < 1) return 0;
if (line > a.length) return ed.ta.value.length;
var s = a[line - 1], e = line < a.length ? a[line] - 1 : ed.ta.value.length;
return Math.min(s + col, e);
}
var metricCache = {};
function metrics(ed) {
var key = S.prefs.fs;
if (metricCache[key]) return metricCache[key];
var probe = el('span', '', '0000000000000000000000000000000000000000');
probe.style.position = 'absolute';
probe.style.visibility = 'hidden';
probe.style.whiteSpace = 'pre';
probe.className = 'hl';
probe.style.padding = '0';
probe.style.minHeight = '0';
ed.hl.parentNode.appendChild(probe);
var w = probe.getBoundingClientRect().width / 40;
probe.remove();
var lh = parseFloat(getComputedStyle(ed.ta).lineHeight) || 20;
metricCache[key] = { cw: w || 8, lh: lh };
return metricCache[key];
}
// Scrolls the editor so that the given unit index is visible.
function revealUnit(ed, idx) {
var m = metrics(ed), sc = ed.scroller;
var line = lineOf(ed, idx);
var ls = lineStarts(ed)[line];
var col = 0, pre = ed.ta.value.slice(ls, idx);
for (var i = 0; i < pre.length; i++) col = pre.charAt(i) === '\t' ? (Math.floor(col / 4) + 1) * 4 : col + 1;
var y = 8 + line * m.lh;
var gw = ed.gutter.offsetWidth;
var x = gw + 10 + col * m.cw;
if (y < sc.scrollTop + m.lh) sc.scrollTop = Math.max(0, y - m.lh * 2);
else if (y + m.lh * 2 > sc.scrollTop + sc.clientHeight) sc.scrollTop = y + m.lh * 3 - sc.clientHeight;
if (x < sc.scrollLeft + gw + 30) sc.scrollLeft = Math.max(0, x - gw - 60);
else if (x + 40 > sc.scrollLeft + sc.clientWidth) sc.scrollLeft = x + 80 - sc.clientWidth;
}
function caretInfo(ed) {
var p = ed.ta.selectionEnd, line = lineOf(ed, p);
return { line: line + 1, col: p - lineStarts(ed)[line] + 1 };
}
/* ---------------------------------------------------------------- painting */
function paintEditor(ed) {
var t = ed.ta.value, n = t.length;
var big = n > 800000;
var cls = (ed.tab.kind === 'grammar' && !big) ? g4Classes(t) : null;
var mk = new Uint8Array(n + 1);
function mark(list, bit) {
for (var i = 0; i < list.length; i++) {
var r = list[i];
for (var k = Math.max(0, r.s); k < Math.min(n, r.e); k++) mk[k] |= (r.bit || bit);
}
}
if (!big) {
mark(ed.diag, 1);
if (ed.find) {
for (var f = 0; f < ed.find.matches.length && f < 5000; f++) {
var fm = ed.find.matches[f];
for (var q = fm[0]; q < fm[1]; q++) mk[q] |= (f === ed.find.cur ? 16 : 8);
}
}
var rng = ed.hover || ed.pin;
if (rng) for (var h = Math.max(0, rng[0]); h < Math.min(n, rng[1]); h++) mk[h] |= 4;
}
var frag = document.createDocumentFragment();
var i = 0;
while (i < n) {
var key = (cls ? cls[i] : 0) | (mk[i] << 4), j = i + 1;
while (j < n && ((cls ? cls[j] : 0) | (mk[j] << 4)) === key) j++;
var txt = t.slice(i, j);
if (key === 0) {
frag.appendChild(document.createTextNode(txt));
} else {
var names = [];
if (key & 15) names.push(CLS[key & 15]);
for (var m = 0; m < MKCLS.length; m++) if ((key >> 4) & MKCLS[m][0]) names.push(MKCLS[m][1]);
frag.appendChild(el('span', names.join(' '), txt));
}
i = j;
}
frag.appendChild(document.createTextNode('\n')); // matches the textarea's trailing line
clear(ed.hl);
ed.hl.appendChild(frag);
var lines = lineStarts(ed).length;
if (lines !== ed.lines) {
var nums = [];
for (var L = 1; L <= lines; L++) nums.push(L);
ed.gutter.textContent = nums.join('\n') + '\n';
ed.lines = lines;
}
}
/* ----------------------------------------------------------------- editing */
function replaceRange(ed, a, b, text) {
var ta = ed.ta;
ta.focus();
ta.setSelectionRange(a, b);
var ok = false;
// execCommand keeps the undo history but is very slow for big multi-line
// strings in Chrome, so large edits take the direct route.
if (text.length + (b - a) <= 20000) {
try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }
}
if (!ok) {
ta.setRangeText(text, a, b, 'end');
ta.dispatchEvent(new Event('input', { bubbles: true }));
}
}
function lineSpan(t, a, b) {
if (b > a && t.charAt(b - 1) === '\n') b--;
var s = t.lastIndexOf('\n', a - 1) + 1;
var e = t.indexOf('\n', b);
if (e < 0) e = t.length;
return [s, e];
}
function indentSelection(ed, outdent) {
var ta = ed.ta, t = ta.value, a = ta.selectionStart, b = ta.selectionEnd;
var unit = ed.tab.kind === 'grammar' ? ' ' : '\t';
if (a === b && !outdent) {
if (unit === '\t') { replaceRange(ed, a, b, '\t'); return; }
var col = a - (t.lastIndexOf('\n', a - 1) + 1);
replaceRange(ed, a, b, unit.slice(0, 4 - (col % 4)));
return;
}
var sp = lineSpan(t, a, b);
var lines = t.slice(sp[0], sp[1]).split('\n').map(function (l) {
if (!outdent) return l.length ? unit + l : l;
if (l.charAt(0) === '\t') return l.slice(1);
var m = /^ {1,4}/.exec(l);
return m ? l.slice(m[0].length) : l;
});
var out = lines.join('\n');
replaceRange(ed, sp[0], sp[1], out);
ed.ta.setSelectionRange(sp[0], sp[0] + out.length);
}
function toggleComment(ed) {
if (ed.tab.kind !== 'grammar') return;
var ta = ed.ta, t = ta.value, sp = lineSpan(t, ta.selectionStart, ta.selectionEnd);
var lines = t.slice(sp[0], sp[1]).split('\n');
var live = lines.filter(function (l) { return l.trim().length; });
var all = live.length && live.every(function (l) { return /^\s*\/\//.test(l); });
var indent = Math.min.apply(null, live.map(function (l) { return /^\s*/.exec(l)[0].length; }).concat([1e9]));
var out = lines.map(function (l) {
if (!l.trim().length) return l;
return all ? l.replace(/^(\s*)\/\/ ?/, '$1') : l.slice(0, indent) + '// ' + l.slice(indent);
}).join('\n');
replaceRange(ed, sp[0], sp[1], out);
ta.setSelectionRange(sp[0], sp[0] + out.length);
}
function editorKeys(ed, ev) {
var ta = ed.ta, k = ev.key, mod = ev.ctrlKey || ev.metaKey;
if (k === 'Tab' && !mod && !ev.altKey) { ev.preventDefault(); indentSelection(ed, ev.shiftKey); return; }
if (k === 'Enter' && !mod && !ev.altKey && !ev.shiftKey) {
ev.preventDefault();
var a = ta.selectionStart, t = ta.value, ls = t.lastIndexOf('\n', a - 1) + 1;
var lead = /^[ \t]*/.exec(t.slice(ls, a))[0];
var before = t.slice(ls, a).replace(/\s+$/, '');
var extra = (ed.tab.kind === 'grammar' && /[:({]$/.test(before)) ? ' ' : '';
replaceRange(ed, a, ta.selectionEnd, '\n' + lead + extra);
return;
}
if (mod && k === '/') { ev.preventDefault(); toggleComment(ed); return; }
if (mod && (k === 'f' || k === 'F' || k === 'h' || k === 'H')) { ev.preventDefault(); openFind(ed); return; }
if (k === 'Escape' && ed.findBar && !ed.findBar.hidden) { ev.preventDefault(); closeFind(ed); }
}
/* -------------------------------------------------------------------- find */
function buildFind(ed) {
var bar = el('div', 'find');
bar.setAttribute('role', 'search');
var r1 = el('div', 'row'), r2 = el('div', 'row');
var q = el('input'); q.type = 'text'; q.placeholder = 'Find'; q.setAttribute('aria-label', 'Find');
var cnt = el('span', 'cnt');
var bPrev = el('button', 'btn sm', '\u2191'); bPrev.title = 'Previous (Shift+Enter)';
var bNext = el('button', 'btn sm', '\u2193'); bNext.title = 'Next (Enter)';
var bCase = el('button', 'btn sm', 'Aa'); bCase.title = 'Match case'; bCase.setAttribute('aria-pressed', 'false');
var bX = el('button', 'btn sm', '\u00d7'); bX.title = 'Close (Esc)';
var rep = el('input'); rep.type = 'text'; rep.placeholder = 'Replace'; rep.setAttribute('aria-label', 'Replace with');
var bRep = el('button', 'btn sm', 'Replace');
var bAll = el('button', 'btn sm', 'All');
[bPrev, bNext, bCase, bX, bRep, bAll].forEach(function (b) { b.type = 'button'; });
[q, bPrev, bNext, bCase, cnt, bX].forEach(function (n) { r1.appendChild(n); });
[rep, bRep, bAll].forEach(function (n) { r2.appendChild(n); });
bar.appendChild(r1); bar.appendChild(r2);
ed.root.appendChild(bar);
ed.findBar = bar;
ed.fq = q; ed.fr = rep; ed.fcnt = cnt; ed.fcase = false;
q.oninput = function () { runFind(ed, true); };
q.onkeydown = function (ev) {
if (ev.key === 'Enter') { ev.preventDefault(); stepFind(ed, ev.shiftKey ? -1 : 1); }
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
};
rep.onkeydown = function (ev) {
if (ev.key === 'Enter') { ev.preventDefault(); replaceOne(ed); }
else if (ev.key === 'Escape') { ev.preventDefault(); closeFind(ed); }
};
bPrev.onclick = function () { stepFind(ed, -1); };
bNext.onclick = function () { stepFind(ed, 1); };
bCase.onclick = function () {
ed.fcase = !ed.fcase;
bCase.setAttribute('aria-pressed', ed.fcase ? 'true' : 'false');
runFind(ed, true);
};
bX.onclick = function () { closeFind(ed); };
bRep.onclick = function () { replaceOne(ed); };
bAll.onclick = function () { replaceAll(ed); };
}
function openFind(ed) {
if (!ed.findBar) buildFind(ed);
ed.findBar.hidden = false;
var sel = ed.ta.value.slice(ed.ta.selectionStart, ed.ta.selectionEnd);
if (sel && sel.indexOf('\n') < 0) ed.fq.value = sel;
ed.fq.focus();
ed.fq.select();
runFind(ed, false);
}
function closeFind(ed) {
if (!ed.findBar) return;
ed.findBar.hidden = true;
ed.find = null;
paintEditor(ed);
ed.ta.focus();
}
function findMatches(ed) {
var q = ed.fq.value, out = [];
if (!q) return out;
var hay = ed.fcase ? ed.ta.value : ed.ta.value.toLowerCase();
var needle = ed.fcase ? q : q.toLowerCase();
for (var i = hay.indexOf(needle); i >= 0 && out.length < 5000; i = hay.indexOf(needle, i + Math.max(1, needle.length))) {
out.push([i, i + needle.length]);
}
return out;
}
function runFind(ed, jump) {
var ms = findMatches(ed), cur = -1;
if (ms.length) {
var from = ed.ta.selectionStart;
for (var i = 0; i < ms.length; i++) { if (ms[i][0] >= from) { cur = i; break; } }
if (cur < 0) cur = 0;
}
ed.find = ms.length || ed.fq.value ? { matches: ms, cur: cur } : null;
ed.fcnt.textContent = ed.fq.value ? (ms.length ? (cur + 1) + ' of ' + ms.length : 'No results') : '';
paintEditor(ed);
if (jump && cur >= 0) selectMatch(ed, ms[cur]);
}
function selectMatch(ed, m) {
ed.ta.setSelectionRange(m[0], m[1]);
revealUnit(ed, m[0]);
}
function stepFind(ed, dir) {
if (!ed.find || !ed.find.matches.length) { runFind(ed, true); return; }
var n = ed.find.matches.length;
ed.find.cur = (ed.find.cur + dir + n) % n;
ed.fcnt.textContent = (ed.find.cur + 1) + ' of ' + n;
paintEditor(ed);
selectMatch(ed, ed.find.matches[ed.find.cur]);
}
function replaceOne(ed) {
if (!ed.find || ed.find.cur < 0) { runFind(ed, true); return; }
var m = ed.find.matches[ed.find.cur];
replaceRange(ed, m[0], m[1], ed.fr.value);
ed.fq.focus();
runFind(ed, true);
}
function replaceAll(ed) {
var ms = findMatches(ed);
if (!ms.length) return;
var t = ed.ta.value, out = '', last = 0;
ms.forEach(function (m) { out += t.slice(last, m[0]) + ed.fr.value; last = m[1]; });
out += t.slice(last);
replaceRange(ed, 0, t.length, out);
ed.fq.focus();
runFind(ed, false);
toast('Replaced ' + plural(ms.length, 'match'));
}
/* ------------------------------------------------------------------ editor */
function makeEditor(tab) {
var root = el('div', 'ed');
root.hidden = true;
var banner = el('div', 'banner');
banner.hidden = true;
var scroller = el('div', 'scroller');
var rows = el('div', 'rows');
var gutter = el('pre', 'gutter');
gutter.setAttribute('aria-hidden', 'true');
var content = el('div', 'content');
var hl = el('pre', 'hl');
hl.setAttribute('aria-hidden', 'true');
var ta = el('textarea', 'ta');
ta.spellcheck = false;
ta.setAttribute('wrap', 'off');
ta.setAttribute('autocomplete', 'off');
ta.setAttribute('autocorrect', 'off');
ta.setAttribute('autocapitalize', 'off');
ta.setAttribute('aria-label', 'Editor');
content.appendChild(hl);
content.appendChild(ta);
rows.appendChild(gutter);
rows.appendChild(content);
scroller.appendChild(rows);
root.appendChild(banner);
root.appendChild(scroller);
var ed = { tab: tab, root: root, banner: banner, scroller: scroller, gutter: gutter, hl: hl, ta: ta,
diag: [], hover: null, pin: null, find: null, ls: null, lines: 0, findBar: null, onInput: null, onCaret: null };
ta.addEventListener('input', function () {
ed.ls = null;
ed.diag = [];
ed.pin = null;
if (ed.find) runFind(ed, false); else paintEditor(ed);
revealUnit(ed, ta.selectionEnd);
if (ed.onInput) ed.onInput(ed);
if (ed.onCaret) ed.onCaret(ed);
});
ta.addEventListener('keydown', function (ev) { editorKeys(ed, ev); });
['keyup', 'mouseup', 'focus'].forEach(function (name) {
ta.addEventListener(name, function (ev) {
if (name === 'keyup' && /^(Arrow|Page|Home|End)/.test(ev.key)) revealUnit(ed, ta.selectionEnd);
if (ed.onCaret) ed.onCaret(ed);
});
});
return ed;
}
function setEditorText(ed, text) {
ed.ta.value = text;
ed.ls = null;
ed.diag = [];
ed.pin = null;
ed.find = null;
if (ed.findBar) { ed.findBar.hidden = true; }
paintEditor(ed);
}
function setEditorRange(ed, kind, range) { // kind: 'hover' | 'pin'; range [s,e) in units or null
ed[kind] = range;
paintEditor(ed);
}
+470
View File
@@ -0,0 +1,470 @@
/* ==================================================================== tabs */
var nextTabId = 1;
var NEW_GRAMMAR = 'grammar MyGrammar;\n\nprog : expr EOF ;\n\nexpr : term (\'+\' term)* ;\n\nterm : INT | ID | \'(\' expr \')\' ;\n\nINT : [0-9]+ ;\nID : [a-zA-Z_] [a-zA-Z_0-9]* ;\nWS : [ \\t\\r\\n]+ -> skip ;\n';
function grammarName(text) {
var m = /^\s*(?:lexer\s+|parser\s+)?grammar\s+([A-Za-z_]\w*)\s*;/m.exec(text.slice(0, 20000));
return m ? m[1] : '';
}
function getTab(id) {
for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].id === id) return S.tabs[i];
return null;
}
function tabTitle(t) {
if (t.path) return basename(t.path);
if (t.kind === 'grammar') { var n = grammarName(t.ed.ta.value); return (n || 'Untitled') + '.g4'; }
return 'Input ' + t.num;
}
function firstOfKind(kind) {
for (var i = 0; i < S.tabs.length; i++) if (S.tabs[i].kind === kind) return S.tabs[i];
return null;
}
function ensureLast() {
var g = getTab(S.lastGrammar), i = getTab(S.lastInput);
if (!g || g.kind !== 'grammar') { g = firstOfKind('grammar'); S.lastGrammar = g ? g.id : null; }
if (!i || i.kind !== 'input') { i = firstOfKind('input'); S.lastInput = i ? i.id : null; }
}
function addTab(o) {
var t = { id: 't' + (nextTabId++), kind: o.kind, path: o.path || '', num: 0, saved: o.text, etag: o.etag || '',
eol: o.eol || 'lf', bom: !!o.bom, dirty: false, rev: 0, banner: '', diskEtag: '', shown: '' };
if (!t.path) t.num = ++S.untitled[t.kind];
t.ed = makeEditor(t);
t.ed.onInput = function () { onEdit(t); };
t.ed.onCaret = updatePos;
setEditorText(t.ed, o.text);
$('editors').appendChild(t.ed.root);
S.tabs.push(t);
return t;
}
function onEdit(t) {
t.rev++;
if (t.kind === 'grammar') S.grev++;
t.dirty = t.ed.ta.value !== t.saved;
var sig = (t.dirty ? '*' : '') + tabTitle(t);
if (sig !== t.shown) renderTabs();
refreshPanels();
if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(t.kind === 'grammar' ? 800 : 500);
}
function activate(id, focus) {
var t = getTab(id);
if (!t) return;
S.active = id;
if (t.kind === 'grammar') S.lastGrammar = id; else S.lastInput = id;
ensureLast();
S.tabs.forEach(function (x) { x.ed.root.hidden = x.id !== id; });
$('welcome').hidden = true;
renderTabs();
updateBanner(t);
updatePos();
refreshPanels();
if (focus !== false) t.ed.ta.focus();
rememberSession();
if (S.prefs.autorun) {
var it = getTab(S.lastInput), r = it ? S.results[it.id] : null;
if (S.lastGrammar && it && (!r || r.rev !== it.rev || r.grev !== S.grev || r.main !== S.lastGrammar)) scheduleRun(120);
}
}
function showEmpty() {
S.active = null;
$('welcome').hidden = false;
renderTabs();
updatePos();
refreshPanels();
}
function renderTabs() {
var bar = $('tabs');
clear(bar);
var activeEl = null;
S.tabs.forEach(function (t) {
var title = tabTitle(t);
t.shown = (t.dirty ? '*' : '') + title;
var on = t.id === S.active;
var d = el('div', 'tab');
d.setAttribute('role', 'tab');
d.setAttribute('aria-selected', on ? 'true' : 'false');
d.tabIndex = on ? 0 : -1;
d.title = t.path || 'Not saved to disk yet';
if (t.dirty) { var dot = el('span', 'dot'); dot.title = 'Unsaved changes'; d.appendChild(dot); }
d.appendChild(el('span', '', title));
var kind = el('button', 'kind', t.kind === 'grammar' ? 'G' : 'I');
kind.type = 'button';
kind.title = (t.kind === 'grammar' ? 'Grammar' : 'Input') + ' - click to treat this file as ' + (t.kind === 'grammar' ? 'an input' : 'a grammar');
kind.onclick = function (ev) { ev.stopPropagation(); toggleKind(t); };
d.appendChild(kind);
var x = el('button', 'tx', '\u00d7');
x.type = 'button';
x.setAttribute('aria-label', 'Close ' + title);
x.onclick = function (ev) { ev.stopPropagation(); closeTab(t.id); };
d.appendChild(x);
d.onclick = function () { activate(t.id); };
d.onauxclick = function (ev) { if (ev.button === 1) { ev.preventDefault(); closeTab(t.id); } };
d.onkeydown = function (ev) {
var i = S.tabs.indexOf(t), n = S.tabs.length;
if (ev.key === 'ArrowRight') { ev.preventDefault(); activate(S.tabs[(i + 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); }
else if (ev.key === 'ArrowLeft') { ev.preventDefault(); activate(S.tabs[(i + n - 1) % n].id, false); $('tabs').querySelector('[aria-selected=true]').focus(); }
else if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); activate(t.id); }
};
bar.appendChild(d);
if (on) activeEl = d;
});
if (activeEl && activeEl.scrollIntoView) activeEl.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
function toggleKind(t) {
t.kind = t.kind === 'grammar' ? 'input' : 'grammar';
delete S.results[t.id];
S.grev++;
ensureLast();
paintEditor(t.ed);
if (t.id === S.active) activate(t.id, false); else renderTabs();
refreshPanels();
scheduleRun(200);
}
function closeTab(id, force) {
var t = getTab(id);
if (!t) return Promise.resolve(true);
function proceed() {
var idx = S.tabs.indexOf(t);
S.tabs.splice(idx, 1);
t.ed.root.remove();
delete S.results[id];
if (t.kind === 'grammar') S.grev++;
if (S.lastGrammar === id) S.lastGrammar = null;
if (S.lastInput === id) S.lastInput = null;
ensureLast();
if (S.active === id) {
var next = S.tabs[Math.min(idx, S.tabs.length - 1)];
S.active = null;
if (next) activate(next.id); else showEmpty();
} else {
renderTabs();
refreshPanels();
}
rememberSession();
scheduleRun(300);
}
if (!t.dirty || force) { proceed(); return Promise.resolve(true); }
return ask('Unsaved changes', '"' + tabTitle(t) + '" has unsaved changes.',
[{ label: 'Save', value: 'save', primary: true }, { label: "Don't save", value: 'discard' }, { label: 'Cancel', value: null }])
.then(function (v) {
if (v === 'discard') { proceed(); return true; }
if (v === 'save') return saveTab(t).then(function (ok) { if (ok) proceed(); return ok; });
return false;
});
}
function updatePos() {
var t = getTab(S.active);
var p = $('pos');
if (!t) { p.textContent = ''; return; }
var c = caretInfo(t.ed);
p.textContent = 'Ln ' + c.line + ', Col ' + c.col;
}
function rememberSession() {
if (!S.info) return;
var active = getTab(S.active);
store.set('session:' + S.info.root, {
open: S.tabs.filter(function (t) { return t.path; }).map(function (t) { return { path: t.path, kind: t.kind }; }),
active: active ? active.path : ''
});
}
/* ========================================================== files on disk */
function openFile(path, o) {
var ex = null;
S.tabs.forEach(function (t) { if (t.path === path) ex = t; });
if (ex) { activate(ex.id); return Promise.resolve(ex); }
return api('GET', '/api/file?path=' + encodeURIComponent(path)).then(function (f) {
var t = addTab({ kind: (o && o.kind) || (isGrammarFile(f.path) ? 'grammar' : 'input'), path: f.path, text: f.text,
etag: f.etag, eol: f.eol, bom: f.bom });
S.lastDir = dirname(f.path);
if (f.lossy) toast(basename(f.path) + ' contains bytes that are not valid UTF-8; they were replaced.', 'error');
activate(t.id, !(o && o.noFocus));
return t;
}).catch(function (e) {
toast('Cannot open ' + path + ': ' + e.message, 'error');
return null;
});
}
function saveError(e) {
toast('Save failed: ' + e.message, 'error');
return false;
}
function doSave(t, path, extra) {
var text = t.ed.ta.value;
var body = { path: path, text: text, eol: t.eol, bom: t.bom };
Object.keys(extra || {}).forEach(function (k) { body[k] = extra[k]; });
return api('POST', '/api/save', body).then(function (r) {
t.path = r.path;
t.saved = text;
t.etag = r.etag;
t.dirty = t.ed.ta.value !== text;
t.banner = '';
S.lastDir = dirname(r.path);
renderTabs();
updateBanner(t);
rememberSession();
toast('Saved ' + basename(r.path));
var gn = t.kind === 'grammar' ? grammarName(text) : '';
if (gn && gn !== stem(basename(r.path))) {
toast('ANTLR expects the file to be called ' + gn + '.g4 (the grammar is named ' + gn + ').', 'error');
}
return true;
});
}
function saveTab(t) {
if (!t.path) return saveAs(t);
return doSave(t, t.path, { baseEtag: t.etag }).catch(function (e) {
if (e.status === 409 && e.data && e.data.changed) {
return ask('File changed on disk', basename(t.path) + ' was modified outside this editor after you opened it. Saving now overwrites those changes.',
[{ label: 'Overwrite', value: 'o', primary: true }, { label: 'Cancel', value: null }]).then(function (v) {
if (v !== 'o') return false;
return doSave(t, t.path, { force: true }).catch(saveError);
});
}
return saveError(e);
});
}
function saveAs(t) {
var suggest = t.path ? basename(t.path) : (t.kind === 'grammar' ? tabTitle(t) : 'input' + t.num + '.txt');
return fileDialog({ title: 'Save as', mode: 'save', dir: t.path ? dirname(t.path) : S.lastDir, name: suggest, okLabel: 'Save' }).then(function (path) {
if (!path) return false;
return doSave(t, path, { create: true }).catch(function (e) {
if (e.status === 409 && e.data && e.data.exists) {
return ask('Replace file?', path + ' already exists. Replace it?', [{ label: 'Replace', value: 'r', primary: true }, { label: 'Cancel', value: null }])
.then(function (v) { return v === 'r' ? doSave(t, path, {}).catch(saveError) : false; });
}
return saveError(e);
});
});
}
function saveAll() {
var chain = Promise.resolve(true);
S.tabs.filter(function (t) { return t.dirty; }).forEach(function (t) { chain = chain.then(function () { return saveTab(t); }); });
return chain;
}
function reloadTab(t, quiet) {
if (!t.path) return Promise.resolve(false);
return api('GET', '/api/file?path=' + encodeURIComponent(t.path)).then(function (f) {
var caret = Math.min(t.ed.ta.selectionStart, f.text.length);
setEditorText(t.ed, f.text);
t.ed.ta.setSelectionRange(caret, caret);
t.saved = f.text;
t.etag = f.etag;
t.eol = f.eol;
t.bom = f.bom;
t.dirty = false;
t.banner = '';
t.rev++;
if (t.kind === 'grammar') S.grev++;
renderTabs();
updateBanner(t);
refreshPanels();
toast((quiet ? 'Reloaded ' + basename(t.path) + ' (changed on disk)' : 'Reloaded ' + basename(t.path)));
if (t.kind === 'grammar' || t.id === S.lastInput) scheduleRun(300);
return true;
}).catch(function (e) {
toast('Cannot reload ' + t.path + ': ' + e.message, 'error');
return false;
});
}
function reloadActive() {
var t = getTab(S.active);
if (!t || !t.path) return;
if (!t.dirty) { reloadTab(t, false); return; }
ask('Reload from disk', 'Discard your unsaved changes to ' + tabTitle(t) + ' and reload it?', [{ label: 'Reload', value: 'r', primary: true }, { label: 'Cancel', value: null }])
.then(function (v) { if (v === 'r') reloadTab(t, false); });
}
function newGrammar() {
var t = addTab({ kind: 'grammar', text: NEW_GRAMMAR });
activate(t.id);
scheduleRun(300);
}
function newInput() {
var t = addTab({ kind: 'input', text: '' });
activate(t.id);
}
function updateBanner(t) {
var b = t.ed.banner;
clear(b);
if (!t.banner) { b.hidden = true; return; }
b.hidden = false;
function btn(label, fn) { var x = el('button', 'btn sm', label); x.type = 'button'; x.onclick = fn; return x; }
if (t.banner === 'deleted') {
b.appendChild(el('span', '', 'This file was deleted on disk.'));
b.appendChild(btn('Keep editing', function () { t.banner = ''; t.etag = ''; updateBanner(t); }));
} else {
b.appendChild(el('span', '', 'This file changed on disk.'));
b.appendChild(btn('Reload', function () { reloadTab(t, false); }));
b.appendChild(btn('Keep my version', function () { t.etag = t.diskEtag; t.banner = ''; updateBanner(t); }));
}
}
/* ------------------------------------------------ external change polling */
var polling = false;
function pollDisk() {
if (polling || document.hidden) return;
var paths = S.tabs.filter(function (t) { return t.path; }).map(function (t) { return t.path; });
if (!paths.length) return;
polling = true;
api('POST', '/api/stat', { paths: paths }).then(function (r) {
S.tabs.forEach(function (t) {
if (!t.path || !(t.path in r.etags)) return;
var tag = r.etags[t.path];
if (tag === t.etag) return;
if (tag === null) {
if (t.banner !== 'deleted' && t.etag) { t.banner = 'deleted'; updateBanner(t); }
return;
}
if (!t.dirty) { reloadTab(t, true); return; }
if (t.diskEtag !== tag || t.banner !== 'changed') { t.diskEtag = tag; t.banner = 'changed'; updateBanner(t); }
});
}).catch(function () { /* server gone or busy: try again later */ }).then(function () { polling = false; });
}
setInterval(pollDisk, 2000);
window.addEventListener('focus', pollDisk);
window.addEventListener('beforeunload', function (ev) {
if (S.tabs.some(function (t) { return t.dirty; })) { ev.preventDefault(); ev.returnValue = ''; }
});
/* ======================================================== open / save dialog */
function fileDialog(o) {
var cur = o.dir !== undefined && o.dir !== null ? o.dir : (S.lastDir || '');
var chosen = '';
var entries = [];
var crumbs = el('div', 'crumbs');
var list = el('ul', 'flist');
list.setAttribute('role', 'listbox');
var filter = null, nameIn = null, ctxRef = null;
function load(dir) {
api('GET', '/api/dir?path=' + encodeURIComponent(dir)).then(function (d) {
cur = d.path;
entries = d.entries;
chosen = '';
drawCrumbs();
drawList();
}).catch(function (e) {
toast(e.message, 'error');
if (dir) load('');
});
}
function crumb(label, path) {
var b = el('button', '', label);
b.type = 'button';
b.onclick = function () { load(path); };
return b;
}
function drawCrumbs() {
clear(crumbs);
crumbs.appendChild(crumb(S.info.rootName || 'workspace', ''));
var acc = '';
(cur ? cur.split('/') : []).forEach(function (p) {
acc = joinPath(acc, p);
crumbs.appendChild(el('span', 'muted', '/'));
crumbs.appendChild(crumb(p, acc));
});
}
function drawList() {
clear(list);
var q = filter ? filter.value.toLowerCase() : '';
var shown = entries.filter(function (e) { return !q || e.name.toLowerCase().indexOf(q) >= 0; });
if (!shown.length) list.appendChild(el('li', 'muted', entries.length ? 'No matches' : 'Empty folder'));
shown.forEach(function (e) {
var li = el('li');
li.setAttribute('role', 'option');
li.tabIndex = 0;
li.appendChild(el('span', '', (e.dir ? '\u25b8 ' : '') + e.name));
if (!e.dir) li.appendChild(el('span', 'sz', human(e.size)));
var path = joinPath(cur, e.name);
function select() {
Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-selected'); });
li.setAttribute('aria-selected', 'true');
chosen = path;
if (nameIn) nameIn.value = e.name;
}
li.onclick = function () { if (e.dir) load(path); else select(); };
li.ondblclick = function () { if (!e.dir) { select(); ctxRef.close('ok'); } };
li.onkeydown = function (ev) {
if (ev.key === 'Enter') { ev.preventDefault(); ev.stopPropagation(); if (e.dir) load(path); else { select(); ctxRef.close('ok'); } }
else if (ev.key === ' ') { ev.preventDefault(); if (e.dir) load(path); else select(); }
else if (ev.key === 'ArrowDown' && li.nextSibling) { ev.preventDefault(); li.nextSibling.focus(); }
else if (ev.key === 'ArrowUp' && li.previousSibling) { ev.preventDefault(); li.previousSibling.focus(); }
};
list.appendChild(li);
});
}
return modal({
title: o.title,
build: function (ctx) {
ctxRef = ctx;
var wrap = el('div');
wrap.appendChild(crumbs);
if (o.mode === 'open') {
filter = el('input');
filter.type = 'text';
filter.placeholder = 'Filter';
filter.setAttribute('aria-label', 'Filter files');
filter.setAttribute('data-focus', '1');
filter.oninput = drawList;
filter.onkeydown = function (ev) { if (ev.key === 'ArrowDown' && list.firstChild) { ev.preventDefault(); list.firstChild.focus(); } };
wrap.appendChild(filter);
}
wrap.appendChild(list);
if (o.mode === 'save') {
var lab = el('p', 'muted', 'File name');
lab.style.margin = '10px 0 4px';
wrap.appendChild(lab);
nameIn = el('input');
nameIn.type = 'text';
nameIn.value = o.name || '';
nameIn.setAttribute('aria-label', 'File name');
nameIn.setAttribute('data-focus', '1');
wrap.appendChild(nameIn);
setTimeout(function () { nameIn.focus(); nameIn.select(); }, 0);
}
load(cur);
return wrap;
},
buttons: [
{ label: 'Cancel', value: null },
{ label: o.okLabel || 'Open', value: 'ok', primary: true, check: function () {
if (o.mode === 'open' && !chosen) { toast('Select a file first'); return false; }
if (o.mode === 'save') {
var n = nameIn.value.trim();
if (!n || /[\/\\]/.test(n)) { toast('Enter a file name without slashes'); return false; }
}
return true;
} }
]
}).then(function (v) {
if (v !== 'ok') return null;
S.lastDir = cur;
return o.mode === 'open' ? chosen : joinPath(cur, nameIn.value.trim());
});
}
function openDialog() {
var t = getTab(S.active);
fileDialog({ title: 'Open file', mode: 'open', dir: t && t.path ? dirname(t.path) : S.lastDir, okLabel: 'Open' }).then(function (p) {
if (p) openFile(p);
});
}
+676
View File
@@ -0,0 +1,676 @@
/* ============================================================== preferences */
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function savePrefs() { store.set('prefs', S.prefs); }
function applyPrefs() {
var root = document.documentElement;
if (S.prefs.theme === 'auto') root.removeAttribute('data-theme'); else root.setAttribute('data-theme', S.prefs.theme);
root.style.setProperty('--fs', S.prefs.fs + 'px');
root.style.setProperty('--lh', Math.round(S.prefs.fs * 1.55) + 'px');
metricCache = {};
$('autorun').checked = !!S.prefs.autorun;
}
function applyLayout() {
var d = S.prefs.diagram, o = S.prefs.output;
$('p-diagram').hidden = !d;
$('p-output').hidden = !o;
$('split-h').hidden = !(d && o);
$('right').classList.toggle('one', !(d && o));
$('main').classList.toggle('no-right', !(d || o));
var root = document.documentElement;
root.style.setProperty('--leftw', (clamp(S.prefs.lw || 0.5, 0.2, 0.8) * 100) + '%');
root.style.setProperty('--toph', (clamp(S.prefs.th || 0.62, 0.15, 0.85) * 100) + '%');
setTimeout(function () { if (!V.userMoved) fit(); }, 0);
}
function initSplit(id, vertical) {
var sp = $(id);
sp.addEventListener('pointerdown', function (ev) {
if (ev.button) return;
sp.setPointerCapture(ev.pointerId);
sp.classList.add('drag');
});
sp.addEventListener('pointermove', function (ev) {
if (!sp.classList.contains('drag')) return;
if (vertical) {
var r = $('main').getBoundingClientRect();
S.prefs.lw = clamp((ev.clientX - r.left - 8) / (r.width - 16), 0.2, 0.8);
} else {
var r2 = $('right').getBoundingClientRect();
S.prefs.th = clamp((ev.clientY - r2.top) / r2.height, 0.15, 0.85);
}
applyLayout();
});
var end = function () { if (sp.classList.contains('drag')) { sp.classList.remove('drag'); savePrefs(); } };
sp.addEventListener('pointerup', end);
sp.addEventListener('pointercancel', end);
sp.addEventListener('keydown', function (ev) {
var step = ev.key === (vertical ? 'ArrowRight' : 'ArrowDown') ? 0.02 : ev.key === (vertical ? 'ArrowLeft' : 'ArrowUp') ? -0.02 : 0;
if (!step) return;
ev.preventDefault();
if (vertical) S.prefs.lw = clamp((S.prefs.lw || 0.5) + step, 0.2, 0.8); else S.prefs.th = clamp((S.prefs.th || 0.62) + step, 0.15, 0.85);
applyLayout();
savePrefs();
});
}
/* ================================================================== results */
function currentInput() { return getTab(S.lastInput); }
function currentResult() {
var it = currentInput();
return it ? (S.results[it.id] || null) : null;
}
function isStale() {
var it = currentInput(), r = currentResult();
if (!it || !r) return false;
return r.rev !== it.rev || r.grev !== S.grev || r.main !== S.lastGrammar || !!(S.gres && !S.gres.ok) || !!S.crash;
}
function parseToolLog(log) {
var out = [];
String(log || '').split(/\r?\n/).forEach(function (line) {
var m = /^(error|warning)\((\d+)\): (.*?):(\d+):(\d+): (.*)$/.exec(line);
if (m) { out.push({ sev: m[1], code: m[2], file: basename(m[3].replace(/\\/g, '/')), line: +m[4], col: +m[5], msg: m[6] }); return; }
var m2 = /^(error|warning)\((\d+)\): (.*)$/.exec(line);
if (m2) out.push({ sev: m2[1], code: m2[2], file: '', line: 0, col: 0, msg: m2[3] });
});
return out;
}
function setRange(kind, s, e) {
var it = currentInput();
if (!it) return;
var r = S.results[it.id];
if (s < 0 || !r || r.rev !== it.rev) { setEditorRange(it.ed, kind, null); return; }
var m = cpMap(it.ed.ta.value);
setEditorRange(it.ed, kind, [m(s), m(e + 1)]);
}
function togglePin(s, e) {
var it = currentInput();
if (!it || s < 0) return;
if (S.active !== it.id) activate(it.id, false);
var cur = it.ed.pin, m = cpMap(it.ed.ta.value), a = m(s), b = m(e + 1);
if (cur && cur[0] === a && cur[1] === b) { setEditorRange(it.ed, 'pin', null); return; }
setEditorRange(it.ed, 'pin', [a, b]);
revealUnit(it.ed, a);
}
function applyMarks() {
// input editor: syntax problems of the latest run
S.tabs.forEach(function (t) {
if (t.kind === 'input') {
var r = S.results[t.id], list = [];
if (r && r.rev === t.rev) {
var m = cpMap(t.ed.ta.value);
r.data.diagnostics.forEach(function (d) {
if (d.s >= 0) list.push({ s: m(d.s), e: m(d.e + 1), bit: d.sev === 'warning' ? 2 : 1 });
});
}
t.ed.diag = list;
paintEditor(t.ed);
} else {
var g = S.gres, marks = [];
if (g && g.snap && g.snap.revs[t.id] === t.rev) {
parseToolLog(g.log).forEach(function (p) {
if (p.file !== tabTitle(t) || p.line < 1) return;
var s = unitAt(t.ed, p.line, p.col), e = s, v = t.ed.ta.value;
while (e < v.length && isIdChar(v.charAt(e))) e++;
if (e === s) e = Math.min(v.length, s + 1);
marks.push({ s: s, e: e, bit: p.sev === 'warning' ? 2 : 1 });
});
}
t.ed.diag = marks;
paintEditor(t.ed);
}
});
}
function diagramNote() {
if (!getTab(S.lastGrammar)) return 'Open or create a grammar (File menu) to see a diagram.';
if (!currentInput()) return 'Open or create an input file (File menu). Its parse tree appears here.';
if (S.gres && !S.gres.ok) return 'The grammar has errors. See the Output panel.';
if (S.crash) return S.crash.message;
var r = currentResult();
if (r && r.data.noDiagram) return r.data.noDiagram;
if (S.running) return 'Running...';
return 'Press Run to parse this input.';
}
var shownData = null; // result data currently drawn in the heavy views
function refreshPanels() {
var it = currentInput(), gt = getTab(S.lastGrammar), r = currentResult();
$('dtitle').textContent = 'Diagram' + (it ? ' (' + tabTitle(it) + ')' : '');
$('otitle').textContent = 'Output' + (gt ? ' (' + stem(tabTitle(gt)) + ')' : '');
var chip = $('chip'), kind = null, text = '';
if (S.gres && !S.gres.ok) { kind = 'fail'; text = 'Grammar has errors'; }
else if (S.crash) { kind = 'fail'; text = S.crash.kind === 'timeout' ? 'Run timed out' : S.crash.kind === 'crashed' ? 'Run crashed' : 'Run failed'; }
else if (r) { kind = r.data.status.kind; text = r.data.status.text; }
chip.hidden = !kind;
if (kind) {
chip.className = 'chip ' + kind + (isStale() ? ' stale' : '');
chip.textContent = text;
chip.title = S.crash ? S.crash.message : (r && r.data.status.detail ? r.data.status.detail : '');
}
$('dgm').classList.toggle('stale', isStale());
var data = r ? r.data : null;
if (data !== shownData) { shownData = data; paintResultViews(data); }
else if (!data) paintResultViews(null);
paintProblems();
paintLog();
paintStartRule();
var busy = $('run');
busy.setAttribute('data-busy', S.running ? 'true' : 'false');
busy.textContent = S.running ? 'Running' : 'Run';
busy.disabled = !getTab(S.lastGrammar);
$('o-problems').setAttribute('aria-selected', S.prefs.out === 'problems' ? 'true' : 'false');
$('o-log').setAttribute('aria-selected', S.prefs.out === 'log' ? 'true' : 'false');
$('probview').hidden = S.prefs.out !== 'problems';
$('logview').hidden = S.prefs.out !== 'log';
}
function paintResultViews(data) {
paintDiagram(data);
var tree = $('treeout');
tree.textContent = data && data.tree ? (V.pretty ? data.treePretty : data.tree) : diagramNote();
paintTokens(data);
showView(S.prefs.view);
}
function showView(v) {
S.prefs.view = v;
['diagram', 'tree', 'tokens'].forEach(function (id) {
$('v-' + id).setAttribute('aria-selected', id === v ? 'true' : 'false');
});
$('dgm').hidden = v !== 'diagram';
$('treeview').hidden = v !== 'tree';
$('tokview').hidden = v !== 'tokens';
$('dtools').hidden = v !== 'diagram';
$('ttools').hidden = v !== 'tree';
$('b-pretty').setAttribute('aria-pressed', V.pretty ? 'true' : 'false');
$('b-flat').setAttribute('aria-pressed', V.pretty ? 'false' : 'true');
if (v === 'diagram' && V.needFit) fit();
}
function paintStartRule() {
var wrap = $('startwrap'), sel = $('startrule'), g = S.gres;
var show = !!(g && g.ok && g.hasParser && g.rules.length);
wrap.hidden = !show;
if (!show) return;
var sig = g.name + '|' + g.rules.join(',');
var chosen = S.start[g.name] || g.rules[0];
if (sel.getAttribute('data-sig') !== sig) {
clear(sel);
g.rules.forEach(function (n) { var o = el('option', '', n); o.value = n; sel.appendChild(o); });
sel.setAttribute('data-sig', sig);
}
sel.value = g.rules.indexOf(chosen) >= 0 ? chosen : g.rules[0];
}
/* -------------------------------------------------------------- problems */
function jumpToRange(tab, s, e) {
if (!tab) return;
activate(tab.id, true);
tab.ed.ta.setSelectionRange(s, e);
revealUnit(tab.ed, s);
}
function paintProblems() {
var list = $('problist');
clear(list);
var items = [], g = S.gres, r = currentResult(), it = currentInput();
if (g) {
var parsed = parseToolLog(g.log);
parsed.forEach(function (p) {
items.push({ src: 'grammar', sev: p.sev, loc: p.line ? p.file + ' ' + p.line + ':' + p.col : p.file, msg: p.msg, go: function () {
var t = null;
S.tabs.forEach(function (x) { if (x.kind === 'grammar' && tabTitle(x) === p.file) t = x; });
t = t || getTab(S.lastGrammar);
if (t && p.line) { var s = unitAt(t.ed, p.line, p.col); jumpToRange(t, s, s); }
} });
});
if (!g.ok && !parsed.some(function (p) { return p.sev === 'error'; })) {
items.push({ src: 'grammar', sev: 'error', loc: '', msg: g.error || 'The grammar could not be compiled.', go: null });
}
}
if (S.crash && !(g && !g.ok)) {
items.push({ src: 'runtime', sev: 'error', loc: S.crash.phase ? S.crash.phase : '', msg: S.crash.message, go: null });
}
if (r && it && !(g && !g.ok) && !S.crash) {
r.data.diagnostics.forEach(function (d) {
items.push({ src: d.src, sev: d.sev, loc: d.line ? d.line + ':' + d.col : '', msg: d.msg, go: function () {
if (r.rev !== it.rev) return;
var m = cpMap(it.ed.ta.value);
if (d.s >= 0) { jumpToRange(it, m(d.s), m(d.e + 1)); setEditorRange(it.ed, 'pin', [m(d.s), m(d.e + 1)]); }
else { var u = unitAt(it.ed, d.line, d.col); jumpToRange(it, u, u); }
} });
});
}
$('noprob').hidden = items.length > 0;
var errs = 0, warns = 0;
items.forEach(function (p) {
if (p.sev === 'error') errs++; else warns++;
var li = el('li', 'prob ' + p.sev);
li.tabIndex = 0;
li.appendChild(el('span', 'sev', p.sev));
li.appendChild(el('span', 'loc', p.loc));
li.appendChild(el('span', 'msg', p.msg));
li.appendChild(el('span', 'src', p.src));
if (p.go) {
li.onclick = function () {
Array.prototype.forEach.call(list.children, function (c) { c.removeAttribute('aria-current'); });
li.setAttribute('aria-current', 'true');
p.go();
};
li.onkeydown = function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); li.click(); } };
}
list.appendChild(li);
});
var badge = $('badge');
badge.hidden = !items.length;
badge.className = 'badge' + (errs ? '' : ' warn');
badge.textContent = String(errs || warns);
}
function paintLog() {
var g = S.gres, text = '';
if (g) {
text = g.log && g.log.trim() ? g.log : (g.ok ? 'The ANTLR tool reported nothing.' : (g.error || ''));
if (g.ok) text += (text.slice(-1) === '\n' ? '' : '\n') + '\nGrammar files: ' + (g.entry || []).join(', ') + (g.cached ? ' (unchanged, reused)' : ' (compiled in ' + g.compileMs + ' ms)');
} else {
text = 'Nothing has run yet.';
}
$('logout').textContent = text;
}
/* ---------------------------------------------------------------- tokens */
function paintTokens(data) {
var wrap = $('tokview');
clear(wrap);
var toks = data ? data.tokens : [];
if (!toks.length) { wrap.appendChild(el('p', 'note', data ? 'No tokens.' : diagramNote())); return; }
var table = el('table'), head = el('tr');
['#', 'Text', 'Type', 'Channel', 'Line:col'].forEach(function (h) { head.appendChild(el('th', '', h)); });
var th = el('thead');
th.appendChild(head);
table.appendChild(th);
var body = el('tbody');
toks.slice(0, 5000).forEach(function (t) {
var tr = el('tr', t.ch && t.ch !== 'DEFAULT_TOKEN_CHANNEL' && t.ch !== '0' ? 'side' : '');
tr.appendChild(el('td', 'num', String(t.i)));
tr.appendChild(el('td', 'mono', t.t));
tr.appendChild(el('td', 'mono', t.ty));
tr.appendChild(el('td', 'mono', t.ch));
tr.appendChild(el('td', 'mono', t.l + ':' + t.c));
tr.onmouseenter = function () { setRange('hover', t.s, t.e); };
tr.onmouseleave = function () { setRange('hover', -1, -1); };
tr.onclick = function () { togglePin(t.s, t.e); };
body.appendChild(tr);
});
table.appendChild(body);
wrap.appendChild(table);
if (toks.length > 5000) wrap.appendChild(el('p', 'note', 'Showing the first 5000 of ' + toks.length + ' tokens.'));
}
/* ================================================================== diagram */
var V = { z: 1, x: 0, y: 0, w: 0, h: 0, key: '', owner: '', needFit: true, userMoved: false, pretty: true };
function applyView() { $('stage').style.transform = 'translate(' + V.x + 'px,' + V.y + 'px) scale(' + V.z + ')'; }
function fit() {
var c = $('dgm'), cw = c.clientWidth, ch = c.clientHeight;
if (!V.w || !cw || !ch) return;
var z = Math.max(0.05, Math.min(1.35, (cw - 40) / V.w, (ch - 40) / V.h));
V.z = z;
V.x = (cw - V.w * z) / 2;
V.y = Math.max(20, (ch - V.h * z) / 2);
V.needFit = false;
V.userMoved = false;
applyView();
}
function zoomAt(cx, cy, f) {
var z = clamp(V.z * f, 0.05, 8), k = z / V.z;
V.x = cx - (cx - V.x) * k;
V.y = cy - (cy - V.y) * k;
V.z = z;
V.userMoved = true;
applyView();
}
function zoomCentre(f) { var c = $('dgm'); zoomAt(c.clientWidth / 2, c.clientHeight / 2, f); }
function paintDiagram(data) {
var stage = $('stage'), note = $('dnote'), box = $('dgm');
var svg = data && data.svg ? data.svg : '';
var owner = S.lastInput || '';
if (owner !== V.owner) { V.owner = owner; V.needFit = true; V.userMoved = false; V.key = '\u0000'; }
if (svg && svg === V.key) return;
V.key = svg;
clear(stage);
if (!svg) {
note.textContent = diagramNote();
note.hidden = false;
box.classList.add('empty');
V.w = V.h = 0;
V.needFit = true;
return;
}
note.hidden = true;
box.classList.remove('empty');
stage.innerHTML = svg;
var vb = stage.querySelector('svg').getAttribute('viewBox').split(' ');
V.w = parseFloat(vb[2]);
V.h = parseFloat(vb[3]);
if (V.needFit || !V.userMoved) fit(); else applyView();
}
function svgMarkup(data) {
var NS = 'http://www.w3.org/2000/svg';
var c = $('stage').querySelector('svg').cloneNode(true);
var st = document.createElementNS(NS, 'style');
st.textContent = data.svgStyle;
var bg = document.createElementNS(NS, 'rect');
bg.setAttribute('class', 'al-bg');
bg.setAttribute('width', '100%');
bg.setAttribute('height', '100%');
c.insertBefore(bg, c.firstChild);
c.insertBefore(st, c.firstChild);
return '<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(c);
}
function download(name, blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
}
function exportName(ext) {
var it = currentInput();
return (it ? stem(tabTitle(it)) : 'tree') + '-tree.' + ext;
}
function savePng(data) {
var scale = Math.min(2, 8000 / Math.max(V.w, V.h));
var img = new Image();
img.onload = function () {
var c = document.createElement('canvas');
c.width = Math.max(1, Math.round(V.w * scale));
c.height = Math.max(1, Math.round(V.h * scale));
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
c.toBlob(function (b) { if (b) download(exportName('png'), b); }, 'image/png');
};
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup(data));
}
function initDiagram() {
var box = $('dgm'), stage = $('stage'), drag = null, moved = false;
box.addEventListener('wheel', function (ev) {
if (!V.w) return;
ev.preventDefault();
var r = box.getBoundingClientRect(), unit = ev.deltaMode === 1 ? 16 : 1;
zoomAt(ev.clientX - r.left, ev.clientY - r.top, Math.exp(-ev.deltaY * unit * (ev.ctrlKey ? 0.01 : 0.0015)));
}, { passive: false });
box.addEventListener('pointerdown', function (ev) {
if (ev.button !== 0 || !V.w) return;
drag = { x: ev.clientX, y: ev.clientY, vx: V.x, vy: V.y };
moved = false;
box.setPointerCapture(ev.pointerId);
box.classList.add('drag');
});
box.addEventListener('pointermove', function (ev) {
if (!drag) return;
var dx = ev.clientX - drag.x, dy = ev.clientY - drag.y;
if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
V.x = drag.vx + dx;
V.y = drag.vy + dy;
V.userMoved = true;
applyView();
});
var end = function () { drag = null; box.classList.remove('drag'); };
box.addEventListener('pointerup', end);
box.addEventListener('pointercancel', end);
box.addEventListener('dblclick', fit);
stage.addEventListener('click', function (ev) {
if (moved) return;
var g = ev.target.closest('.al-node');
if (g && g.hasAttribute('data-s')) togglePin(+g.getAttribute('data-s'), +g.getAttribute('data-e'));
});
stage.addEventListener('mouseover', function (ev) {
var g = ev.target.closest('.al-node');
if (g && g.hasAttribute('data-s')) setRange('hover', +g.getAttribute('data-s'), +g.getAttribute('data-e'));
});
stage.addEventListener('mouseout', function (ev) { if (ev.target.closest('.al-node')) setRange('hover', -1, -1); });
box.addEventListener('keydown', function (ev) {
var k = ev.key;
if (k === '+' || k === '=') zoomCentre(1.25);
else if (k === '-') zoomCentre(0.8);
else if (k === '0') fit();
else if (k === 'ArrowLeft') { V.x += 60; V.userMoved = true; applyView(); }
else if (k === 'ArrowRight') { V.x -= 60; V.userMoved = true; applyView(); }
else if (k === 'ArrowUp') { V.y += 60; V.userMoved = true; applyView(); }
else if (k === 'ArrowDown') { V.y -= 60; V.userMoved = true; applyView(); }
else return;
ev.preventDefault();
});
$('zin').onclick = function () { zoomCentre(1.25); };
$('zout').onclick = function () { zoomCentre(0.8); };
$('zfit').onclick = fit;
$('psvg').onclick = function () { var r = currentResult(); if (r && V.w) download(exportName('svg'), new Blob([svgMarkup(r.data)], { type: 'image/svg+xml' })); };
$('ppng').onclick = function () { var r = currentResult(); if (r && V.w) savePng(r.data); };
if (window.ResizeObserver) new ResizeObserver(function () { if (!V.userMoved && V.w) fit(); }).observe(box);
['diagram', 'tree', 'tokens'].forEach(function (id) {
$('v-' + id).onclick = function () { showView(id); savePrefs(); };
});
$('b-pretty').onclick = function () { V.pretty = true; shownData = null; refreshPanels(); };
$('b-flat').onclick = function () { V.pretty = false; shownData = null; refreshPanels(); };
$('b-copy').onclick = function () {
var text = $('treeout').textContent, b = $('b-copy');
var done = function () { var old = b.textContent; b.textContent = 'Copied'; setTimeout(function () { b.textContent = old; }, 1200); };
if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done, function () {});
else {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); done(); } catch (e) { /* ignore */ }
ta.remove();
}
};
$('o-problems').onclick = function () { S.prefs.out = 'problems'; savePrefs(); refreshPanels(); };
$('o-log').onclick = function () { S.prefs.out = 'log'; savePrefs(); refreshPanels(); };
}
/* ====================================================================== run */
var runTimer = null;
function scheduleRun(ms) {
clearTimeout(runTimer);
if (!S.prefs.autorun) return;
runTimer = setTimeout(runNow, ms);
}
function runNow() {
clearTimeout(runTimer);
ensureLast();
var main = getTab(S.lastGrammar), inp = getTab(S.lastInput);
if (!main) { S.gres = null; refreshPanels(); return; }
if (S.running) { S.queued = true; return; }
S.running = true;
var snap = { revs: {}, inputRev: inp ? inp.rev : 0, grev: S.grev, inputId: inp ? inp.id : null };
var grammars = S.tabs.filter(function (t) { return t.kind === 'grammar'; });
grammars.forEach(function (t) { snap.revs[t.id] = t.rev; });
var name = stem(tabTitle(main));
var body = {
grammars: grammars.map(function (t) { return { name: tabTitle(t), path: t.path, text: t.ed.ta.value }; }),
main: tabTitle(main),
start: S.start[name] || ''
};
if (inp) body.input = { name: tabTitle(inp), text: inp.ed.ta.value };
refreshPanels();
api('POST', '/api/run', body).then(function (r) {
var g = r.grammar;
S.gres = { ok: !!g.ok, name: g.name, log: g.log || '', error: g.error || '', rules: g.rules || [], hasParser: !!g.hasParser,
entry: g.entry || [], cached: !!g.cached, compileMs: g.compileMs, snap: snap };
if (r.result && inp) S.results[inp.id] = { data: r.result, rev: snap.inputRev, grev: snap.grev, main: main.id };
var wasCrash = !!S.crash;
S.crash = r.crash || null;
if (S.crash && !wasCrash) toast(S.crash.kind === 'timeout' ? 'The run took too long and was stopped.' : 'The ANTLR runtime failed on this input. See Problems.', 'error');
if (r.runError) toast(r.runError, 'error');
if (!g.ok && S.prefs.out !== 'problems') { S.prefs.out = 'problems'; }
}).catch(function (e) {
S.gres = { ok: false, name: name, log: '', error: e.message, rules: [], hasParser: false, entry: [], cached: false, compileMs: 0, snap: snap };
toast('Run failed: ' + e.message, 'error');
}).then(function () {
S.running = false;
shownData = null;
applyMarks();
refreshPanels();
if (S.queued) { S.queued = false; runNow(); }
});
}
/* =================================================================== wiring */
function withEditor(fn) {
var t = getTab(S.active);
if (t) fn(t);
}
function changeFont(delta) {
S.prefs.fs = delta === 0 ? 13 : clamp(S.prefs.fs + delta, 10, 24);
applyPrefs();
savePrefs();
}
function setPref(key, value) {
S.prefs[key] = value;
applyPrefs();
applyLayout();
savePrefs();
}
function buildMenus() {
var hasTab = function () { return !!getTab(S.active); };
defineMenus([
{ label: 'File', items: function () {
var t = getTab(S.active);
return [
{ label: 'New grammar', action: newGrammar },
{ label: 'New input', action: newInput },
{ sep: true },
{ label: 'Open\u2026', key: 'Ctrl+O', action: openDialog },
{ sep: true },
{ label: 'Save', key: 'Ctrl+S', disabled: !t, action: function () { withEditor(saveTab); } },
{ label: 'Save as\u2026', key: 'Ctrl+Shift+S', disabled: !t, action: function () { withEditor(saveAs); } },
{ label: 'Save all', disabled: !S.tabs.some(function (x) { return x.dirty; }), action: saveAll },
{ sep: true },
{ label: 'Reload from disk', disabled: !(t && t.path), action: reloadActive },
{ label: 'Close tab', key: 'Alt+W', disabled: !t, action: function () { withEditor(function (x) { closeTab(x.id); }); } }
];
} },
{ label: 'Edit', items: function () {
return [
{ label: 'Undo', key: 'Ctrl+Z', disabled: !hasTab(), action: function () { withEditor(function (t) { t.ed.ta.focus(); document.execCommand('undo'); }); } },
{ label: 'Redo', key: 'Ctrl+Y', disabled: !hasTab(), action: function () { withEditor(function (t) { t.ed.ta.focus(); document.execCommand('redo'); }); } },
{ sep: true },
{ label: 'Find and replace', key: 'Ctrl+F', disabled: !hasTab(), action: function () { withEditor(function (t) { openFind(t.ed); }); } },
{ label: 'Toggle comment', key: 'Ctrl+/', disabled: !(getTab(S.active) && getTab(S.active).kind === 'grammar'), action: function () { withEditor(function (t) { t.ed.ta.focus(); toggleComment(t.ed); }); } }
];
} },
{ label: 'View', items: function () {
return [
{ label: 'Diagram panel', checked: S.prefs.diagram, action: function () { setPref('diagram', !S.prefs.diagram); } },
{ label: 'Output panel', checked: S.prefs.output, action: function () { setPref('output', !S.prefs.output); } },
{ sep: true },
{ label: 'Auto-run', checked: S.prefs.autorun, action: function () { setPref('autorun', !S.prefs.autorun); if (S.prefs.autorun) scheduleRun(100); } },
{ sep: true },
{ label: 'Theme: match system', checked: S.prefs.theme === 'auto', action: function () { setPref('theme', 'auto'); } },
{ label: 'Theme: light', checked: S.prefs.theme === 'light', action: function () { setPref('theme', 'light'); } },
{ label: 'Theme: dark', checked: S.prefs.theme === 'dark', action: function () { setPref('theme', 'dark'); } },
{ sep: true },
{ label: 'Larger text', action: function () { changeFont(1); } },
{ label: 'Smaller text', action: function () { changeFont(-1); } },
{ label: 'Reset text size', action: function () { changeFont(0); } }
];
} }
]);
}
function initShortcuts() {
document.addEventListener('keydown', function (ev) {
if ($('modal-root').firstChild) return;
var mod = ev.ctrlKey || ev.metaKey, k = ev.key;
if (mod && k === 'Enter') { ev.preventDefault(); runNow(); }
else if (mod && (k === 's' || k === 'S')) { ev.preventDefault(); withEditor(ev.shiftKey ? saveAs : saveTab); }
else if (mod && (k === 'o' || k === 'O')) { ev.preventDefault(); openDialog(); }
else if (ev.altKey && (k === 'w' || k === 'W')) { ev.preventDefault(); withEditor(function (t) { closeTab(t.id); }); }
else if (k === 'Escape') closeMenu(true);
});
}
function fatal(e) {
var m = $('main');
clear(m);
m.style.display = 'block';
var box = el('div', 'note');
box.appendChild(el('h2', '', 'Cannot reach antlrlab'));
box.appendChild(el('p', '', (e && e.message ? e.message + '. ' : '') + 'The server may have stopped, or this page was opened without its token. Restart antlrlab and open the address it prints.'));
m.appendChild(box);
}
function boot() {
var saved = store.get('prefs', null);
if (saved) Object.keys(saved).forEach(function (k) { S.prefs[k] = saved[k]; });
buildMenus();
applyPrefs();
applyLayout();
initDiagram();
initSplit('split-v', true);
initSplit('split-h', false);
initShortcuts();
$('run').onclick = runNow;
$('autorun').onchange = function () { setPref('autorun', this.checked); if (S.prefs.autorun) scheduleRun(100); };
$('startrule').onchange = function () {
var g = S.gres;
if (g) S.start[g.name] = this.value;
runNow();
};
$('x-diagram').onclick = function () { setPref('diagram', false); };
$('x-output').onclick = function () { setPref('output', false); };
$('w-open').onclick = openDialog;
$('w-grammar').onclick = newGrammar;
$('w-input').onclick = newInput;
showView(S.prefs.view || 'diagram');
refreshPanels();
api('GET', '/api/info').then(function (info) {
S.info = info;
$('rootname').textContent = info.rootName;
$('rootname').title = info.root;
if (!info.tool.found) toast(info.tool.error, 'error');
var session = store.get('session:' + info.root, null);
var fromCli = info.open.length > 0;
var list = fromCli ? info.open.map(function (p) { return { path: p }; }) : (session ? session.open : []);
var chain = Promise.resolve();
list.forEach(function (f) { chain = chain.then(function () { return openFile(f.path, { noFocus: true, kind: fromCli ? undefined : f.kind }); }); });
return chain.then(function () {
if (!S.tabs.length) { showEmpty(); return; }
var pick = null;
if (!fromCli && session && session.active) S.tabs.forEach(function (t) { if (t.path === session.active) pick = t; });
pick = pick || firstOfKind('input') || S.tabs[0];
activate(pick.id, true);
if (firstOfKind('grammar')) { S.lastGrammar = firstOfKind('grammar').id; }
scheduleRun(50);
});
}).catch(fatal);
}
boot();
})();
+189
View File
@@ -0,0 +1,189 @@
:root{--bg:#e6ecf4;--pane:#ffffff;--edit:#ffffff;--ink:#1b2a41;--muted:#5b6a80;--line:#cbd5e3;
--title:#cfe2f5;--title-ink:#0e2a45;--tab:#eef2f8;--hov:#e3ebf6;--hl:rgba(36,86,211,.2);--hl2:#dbe7fb;
--accent:#2456d3;--onaccent:#ffffff;--ok:#1a7f45;--err:#c62f2a;--warn:#a86a06;--onstatus:#ffffff;
--okbg:#e5f4ea;--errbg:#fbe7e5;--warnbg:#fbf0d9;--findbg:#fff0a8;--curbg:#ffd25e;--dot:#cdd5e1;
--c-com:#6b7c93;--c-str:#1f7a3d;--c-set:#a35a00;--c-act:#6b4fb3;--c-ann:#a02a74;--c-kw:#1c4fc4;--c-tok:#0b7a66;--c-lab:#4a5d78;--fs:13px;--lh:20px}
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--bg:#08111f;--pane:#0f1c30;--edit:#0b1626;--ink:#e4ecf8;--muted:#8a9db8;--line:#233752;
--title:#17476b;--title-ink:#eef6ff;--tab:#13233a;--hov:#1a2e4a;--hl:rgba(110,168,255,.28);--hl2:#1d3a5e;
--accent:#6ea8ff;--onaccent:#07162b;--ok:#4cc38a;--err:#ff7b72;--warn:#e3b341;--onstatus:#08111f;
--okbg:#0f2a27;--errbg:#2e1a24;--warnbg:#2b2617;--findbg:#4d4318;--curbg:#7a5f12;--dot:#1b2d48;
--c-com:#7e90aa;--c-str:#9ad0a0;--c-set:#e6b673;--c-act:#b39ddb;--c-ann:#f0a8d0;--c-kw:#7fb2ff;--c-tok:#7dd3c0;--c-lab:#9db4d6;}}
:root[data-theme=dark]{--bg:#08111f;--pane:#0f1c30;--edit:#0b1626;--ink:#e4ecf8;--muted:#8a9db8;--line:#233752;
--title:#17476b;--title-ink:#eef6ff;--tab:#13233a;--hov:#1a2e4a;--hl:rgba(110,168,255,.28);--hl2:#1d3a5e;
--accent:#6ea8ff;--onaccent:#07162b;--ok:#4cc38a;--err:#ff7b72;--warn:#e3b341;--onstatus:#08111f;
--okbg:#0f2a27;--errbg:#2e1a24;--warnbg:#2b2617;--findbg:#4d4318;--curbg:#7a5f12;--dot:#1b2d48;
--c-com:#7e90aa;--c-str:#9ad0a0;--c-set:#e6b673;--c-act:#b39ddb;--c-ann:#f0a8d0;--c-kw:#7fb2ff;--c-tok:#7dd3c0;--c-lab:#9db4d6;}
*{box-sizing:border-box}
[hidden]{display:none!important}
html,body{height:100%;margin:0}
body{display:flex;flex-direction:column;background:var(--bg);color:var(--ink);overflow:hidden;
font:13px/1.4 system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',sans-serif}
button,input,select,textarea{font:inherit;color:inherit}
pre,code,.mono,.ta,.hl,.gutter,.out,td.mono,#pos{font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,'DejaVu Sans Mono','Liberation Mono',monospace}
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.spacer{flex:1}
.muted{color:var(--muted)}
/* menu bar */
#menubar{display:flex;align-items:center;gap:2px;height:32px;padding:0 8px;background:var(--pane);border-bottom:1px solid var(--line);flex:none}
.menu-btn{padding:4px 10px;background:none;border:0;border-radius:3px;cursor:pointer}
.menu-btn:hover,.menu-btn[aria-expanded=true]{background:var(--hov)}
#rootname{margin-left:auto;color:var(--muted);font-size:12px;padding-right:6px}
.dropdown{position:fixed;z-index:60;min-width:230px;padding:4px 0;background:var(--pane);border:1px solid var(--line)}
.mi{display:flex;align-items:center;gap:14px;padding:5px 14px 5px 26px;cursor:pointer;position:relative;white-space:nowrap}
.mi:hover,.mi:focus{background:var(--hl2);outline:0}
.mi .k{margin-left:auto;padding-left:24px;color:var(--muted);font-size:12px}
.mi .ck{position:absolute;left:8px}
.mi[aria-disabled=true]{opacity:.45;pointer-events:none}
.sep{height:1px;margin:4px 0;background:var(--line)}
/* layout */
main{flex:1;min-height:0;display:grid;grid-template-columns:var(--leftw,50%) 8px minmax(0,1fr);padding:8px}
main.no-right{grid-template-columns:minmax(0,1fr)}
main.no-right #split-v,main.no-right #right{display:none}
#right{display:grid;grid-template-rows:var(--toph,62%) 8px minmax(0,1fr);min-height:0;min-width:0}
#right.one{grid-template-rows:minmax(0,1fr)}
.split{background:none;position:relative}
.split::after{content:"";position:absolute;background:var(--line);opacity:0;transition:opacity .1s}
.split-v::after{left:3px;right:3px;top:0;bottom:0}
.split-h::after{top:3px;bottom:3px;left:0;right:0}
.split:hover::after,.split:focus-visible::after,.split.drag::after{opacity:1;background:var(--accent)}
.split-v{cursor:col-resize}
.split-h{cursor:row-resize}
.pane{display:flex;flex-direction:column;min-height:0;min-width:0;background:var(--pane);border:1px solid var(--line)}
.ptitle{display:flex;align-items:center;gap:10px;flex:none;padding:5px 6px 5px 12px;background:var(--title);color:var(--title-ink);font-weight:600}
.x{background:none;border:0;border-radius:3px;padding:0 6px;font-size:17px;line-height:1.1;cursor:pointer;opacity:.75}
.x:hover{opacity:1;background:rgba(127,127,127,.25)}
.psub{display:flex;align-items:center;gap:8px;flex:none;min-height:36px;padding:4px 8px;border-bottom:1px solid var(--line)}
.seg{display:flex;border:1px solid var(--line);border-radius:4px;overflow:hidden}
.seg button{border:0;background:none;padding:3px 11px;color:var(--muted);cursor:pointer;white-space:nowrap}
.seg button:hover{color:var(--ink)}
.seg button[aria-selected=true]{background:var(--hl2);color:var(--ink);font-weight:600}
.tools{display:flex;gap:5px}
.btn{padding:4px 12px;background:var(--pane);border:1px solid var(--line);border-radius:4px;cursor:pointer;white-space:nowrap}
.btn:hover{border-color:var(--accent)}
.btn.sm{padding:2px 9px}
.btn[aria-pressed=true]{background:var(--hl2);border-color:var(--accent)}
.btn.primary{background:var(--accent);color:var(--onaccent);border-color:var(--accent);font-weight:600}
.view{flex:1;min-height:0;position:relative;overflow:hidden}
.scroll{overflow:auto;padding:10px 14px}
.flush{padding:0}
.note{margin:0;padding:18px;color:var(--muted)}
.out{margin:0;font-size:12.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
.chip{padding:0 9px;border-radius:9px;font-size:12px;font-weight:600;background:var(--ok);color:var(--onstatus)}
.chip.warn{background:var(--warn)}
.chip.error,.chip.fail{background:var(--err)}
.chip.stale{opacity:.55}
.badge{margin-left:6px;padding:0 6px;border-radius:9px;background:var(--err);color:var(--onstatus);font-size:11px;font-weight:600}
.badge.warn{background:var(--warn)}
/* tabs and toolbar */
#tabs{display:flex;flex:none;overflow-x:auto;background:var(--tab);border-bottom:1px solid var(--line)}
.tab{display:flex;align-items:center;gap:8px;padding:6px 6px 6px 12px;background:var(--tab);color:var(--muted);border-right:1px solid var(--line);cursor:pointer;white-space:nowrap;user-select:none}
.tab:hover{color:var(--ink)}
.tab[aria-selected=true]{background:var(--title);color:var(--title-ink);font-weight:600}
.tab .kind{font:600 11px ui-monospace,Menlo,Consolas,monospace;padding:0 4px;border:1px solid currentColor;border-radius:2px;background:none;color:inherit;opacity:.8;cursor:pointer}
.tab .kind:hover{opacity:1}
.tab .dot{width:8px;height:8px;border-radius:50%;background:var(--warn)}
.tab .tx{background:none;border:0;border-radius:3px;padding:0 5px;font-size:15px;line-height:1.1;cursor:pointer;color:inherit;opacity:.7}
.tab .tx:hover{opacity:1;background:rgba(127,127,127,.25)}
#toolbar{display:flex;align-items:center;gap:14px;flex:none;min-height:36px;padding:4px 8px;border-bottom:1px solid var(--line)}
.tb-item{display:flex;align-items:center;gap:6px;color:var(--muted)}
.tb-item select{background:var(--pane);border:1px solid var(--line);border-radius:4px;padding:2px 4px;max-width:180px}
#pos{font-size:12px}
.runbtn{padding:4px 18px;border:0;border-radius:4px;background:var(--accent);color:var(--onaccent);font-weight:600;cursor:pointer}
.runbtn[data-busy=true]{opacity:.6}
.runbtn:disabled{opacity:.4;cursor:default}
/* editor */
#editors{flex:1;min-height:0;position:relative;background:var(--edit)}
.ed{position:absolute;inset:0;display:flex;flex-direction:column}
.banner{display:flex;align-items:center;gap:10px;flex:none;padding:6px 12px;background:var(--warnbg);border-bottom:1px solid var(--line)}
.scroller{flex:1;min-height:0;overflow:auto;position:relative}
.rows{display:grid;grid-template-columns:auto 1fr;width:max-content;min-width:100%;min-height:100%}
.gutter,.hl,.ta{font-size:var(--fs);line-height:var(--lh);tab-size:4;font-variant-ligatures:none;letter-spacing:normal}
.gutter{position:sticky;left:0;z-index:1;margin:0;padding:8px 10px 8px 14px;text-align:right;color:var(--muted);background:var(--edit);border-right:1px solid var(--line);user-select:none}
.content{position:relative;min-width:0}
.hl{margin:0;padding:8px 28px 8px 10px;min-height:100%;white-space:pre;color:var(--ink);pointer-events:none}
.ta{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:8px 28px 8px 10px;border:0;resize:none;overflow:hidden;white-space:pre;background:transparent;color:transparent;caret-color:var(--ink);outline:0}
.ta::selection{background:rgba(110,168,255,.38);color:transparent}
.t-com{color:var(--c-com);font-style:italic}
.t-str{color:var(--c-str)}
.t-set{color:var(--c-set)}
.t-act{color:var(--c-act)}
.t-ann{color:var(--c-ann)}
.t-kw{color:var(--c-kw);font-weight:600}
.t-tok{color:var(--c-tok)}
.t-def{font-weight:700}
.t-tokdef{color:var(--c-tok);font-weight:700}
.t-lab{color:var(--c-lab);font-style:italic}
.t-op{color:var(--muted)}
.mk-err{background:var(--errbg);text-decoration:underline wavy var(--err);text-underline-offset:3px}
.mk-warn{text-decoration:underline wavy var(--warn);text-underline-offset:3px}
.mk-hl{background:var(--hl);box-shadow:0 0 0 1px var(--accent);border-radius:2px}
.mk-find{background:var(--findbg)}
.mk-cur{background:var(--curbg)}
.find{position:absolute;top:6px;right:22px;z-index:4;display:grid;gap:6px;padding:8px;background:var(--pane);border:1px solid var(--line)}
.find .row{display:flex;gap:5px;align-items:center}
.find input{width:190px;padding:3px 6px;background:var(--edit);border:1px solid var(--line);border-radius:3px}
.find .cnt{min-width:58px;color:var(--muted);font-size:12px}
#welcome{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--muted);text-align:center;padding:20px}
#welcome h2{margin:0;color:var(--ink);font-size:18px}
#welcome p{margin:0 0 10px}
.wbtns{display:flex;gap:8px}
/* diagram */
#dgm{cursor:grab;touch-action:none;background-color:var(--bg);background-image:radial-gradient(var(--dot) 1px,transparent 1px);background-size:22px 22px}
#dgm.drag{cursor:grabbing}
#dgm.empty{cursor:default}
#dgm.stale #stage{opacity:.45}
#stage{position:absolute;left:0;top:0;transform-origin:0 0}
#stage svg{display:block;overflow:visible}
#stage .al-node{cursor:pointer}
#dnote{position:absolute;left:0;right:0;top:0}
/* tokens, problems */
table{border-collapse:collapse;width:100%;font-size:12.5px}
th{position:sticky;top:-10px;background:var(--pane);text-align:left;font-weight:600;color:var(--muted);padding:5px 10px;border-bottom:1px solid var(--line)}
td{padding:3px 10px;border-bottom:1px solid var(--line);vertical-align:top}
tbody tr:hover{background:var(--hl)}
tr.side td{opacity:.55}
td.num{text-align:right;color:var(--muted)}
.probs{list-style:none;margin:0;padding:0}
.prob{display:grid;grid-template-columns:60px 130px minmax(0,1fr) auto;gap:10px;padding:7px 14px;border-bottom:1px solid var(--line);cursor:pointer}
.prob:hover,.prob[aria-current=true]{background:var(--hl)}
.prob .sev{font-weight:600;color:var(--err)}
.prob.warning .sev{color:var(--warn)}
.prob .loc{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--muted);overflow-wrap:anywhere}
.prob .src{color:var(--muted);font-size:12px}
/* dialogs, toasts */
.modal-back{position:fixed;inset:0;z-index:80;display:flex;align-items:flex-start;justify-content:center;padding-top:12vh;background:rgba(5,10,20,.5)}
.modal{width:min(560px,92vw);max-height:76vh;display:flex;flex-direction:column;background:var(--pane);border:1px solid var(--line)}
.modal h3{margin:0;padding:10px 14px;font-size:14px;background:var(--title);color:var(--title-ink)}
.mbody{padding:12px 14px;overflow:auto;min-height:0}
.mbody p{margin:0 0 6px}
.mfoot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--line)}
.crumbs{display:flex;flex-wrap:wrap;gap:2px;margin-bottom:8px;font-size:12.5px}
.crumbs button{background:none;border:0;padding:1px 5px;border-radius:3px;cursor:pointer;color:var(--accent)}
.crumbs button:hover{background:var(--hov)}
.flist{list-style:none;margin:8px 0 0;padding:0;height:260px;overflow:auto;border:1px solid var(--line);background:var(--edit)}
.flist li{display:flex;gap:10px;padding:4px 10px;cursor:pointer}
.flist li:hover{background:var(--hov)}
.flist li[aria-selected=true]{background:var(--hl2)}
.flist .sz{margin-left:auto;color:var(--muted);font-size:12px}
.mbody input[type=text]{width:100%;padding:5px 8px;background:var(--edit);border:1px solid var(--line);border-radius:4px}
#toasts{position:fixed;left:14px;bottom:14px;z-index:90;display:flex;flex-direction:column;gap:6px}
.toast{padding:7px 12px;background:var(--title);color:var(--title-ink);border:1px solid var(--line);max-width:420px}
.toast.error{background:var(--err);color:var(--onstatus)}
@media (max-width:900px){
body{overflow:auto}
main{display:block;overflow:visible}
.split{display:none}
#left{height:70vh}
#right{display:block;margin-top:8px}
#right>.panel{height:70vh;margin-bottom:8px}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}
+85
View File
@@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>antlrlab</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2317476b'/%3E%3Cpath d='M8 3.5v3.5M4 12.5V9.5h8v3M8 7v2.5' stroke='%23fff' fill='none' stroke-width='1.4'/%3E%3C/svg%3E">
<link rel="stylesheet" href="/app.css">
</head>
<body>
<header id="menubar" role="menubar" aria-label="Main menu"></header>
<main id="main">
<section id="left" class="pane" aria-label="Editor">
<div id="tabs" role="tablist" aria-label="Open files"></div>
<div id="toolbar">
<label class="tb-item" id="startwrap" hidden>Start rule
<select id="startrule" aria-label="Start rule"></select>
</label>
<label class="tb-item"><input type="checkbox" id="autorun"> Auto-run</label>
<span class="spacer"></span>
<span id="pos" class="muted"></span>
<button id="run" class="runbtn" type="button" title="Run (Ctrl+Enter)">Run</button>
</div>
<div id="editors">
<div id="welcome" hidden>
<h2>No file open</h2>
<p>Open a grammar and an input file from disk, or start with new ones.</p>
<div class="wbtns">
<button type="button" class="btn" id="w-open">Open&hellip;</button>
<button type="button" class="btn" id="w-grammar">New grammar</button>
<button type="button" class="btn" id="w-input">New input</button>
</div>
</div>
</div>
</section>
<div id="split-v" class="split split-v" role="separator" aria-orientation="vertical" aria-label="Resize editor and results" tabindex="0"></div>
<section id="right">
<section id="p-diagram" class="pane panel" aria-label="Diagram">
<div class="ptitle"><span id="dtitle">Diagram</span><span id="chip" class="chip" hidden></span><span class="spacer"></span><button class="x" id="x-diagram" type="button" aria-label="Close diagram panel">&times;</button></div>
<div class="psub">
<div class="seg" role="tablist" aria-label="Result view">
<button type="button" role="tab" id="v-diagram">Diagram</button>
<button type="button" role="tab" id="v-tree">Tree</button>
<button type="button" role="tab" id="v-tokens">Tokens</button>
</div>
<span class="spacer"></span>
<span id="dtools" class="tools">
<button class="btn sm" id="zin" type="button" aria-label="Zoom in">+</button>
<button class="btn sm" id="zout" type="button" aria-label="Zoom out">&minus;</button>
<button class="btn sm" id="zfit" type="button">Fit</button>
<button class="btn sm" id="psvg" type="button">SVG</button>
<button class="btn sm" id="ppng" type="button">PNG</button>
</span>
<span id="ttools" class="tools" hidden>
<button class="btn sm" id="b-pretty" type="button">Indented</button>
<button class="btn sm" id="b-flat" type="button">One line</button>
<button class="btn sm" id="b-copy" type="button">Copy</button>
</span>
</div>
<div id="dgm" class="view" tabindex="0" aria-label="Parse tree diagram. Drag to pan, scroll to zoom.">
<div id="stage"></div>
<p id="dnote" class="note" hidden></p>
</div>
<div id="treeview" class="view scroll" hidden><pre id="treeout" class="out"></pre></div>
<div id="tokview" class="view scroll" hidden></div>
</section>
<div id="split-h" class="split split-h" role="separator" aria-orientation="horizontal" aria-label="Resize diagram and output" tabindex="0"></div>
<section id="p-output" class="pane panel" aria-label="Output">
<div class="ptitle"><span id="otitle">Output</span><span class="spacer"></span><button class="x" id="x-output" type="button" aria-label="Close output panel">&times;</button></div>
<div class="psub">
<div class="seg" role="tablist" aria-label="Output view">
<button type="button" role="tab" id="o-problems">Problems<span class="badge" id="badge" hidden></span></button>
<button type="button" role="tab" id="o-log">ANTLR tool</button>
</div>
</div>
<div id="probview" class="view scroll flush"><p id="noprob" class="note">No problems.</p><ul id="problist" class="probs"></ul></div>
<div id="logview" class="view scroll" hidden><pre id="logout" class="out"></pre></div>
</section>
</section>
</main>
<div id="toasts" aria-live="polite"></div>
<div id="modal-root"></div>
<script src="/app.js"></script>
</body>
</html>