Fix #628: Grammar railroad diagram (#673)

* Fix #628: Add EBNF grammar for railroad diagram generation

Add a formal EBNF grammar file (grammar/chaiscript.ebnf) that can be
pasted into rr (https://www.bottlecaps.de/rr/ui) to produce navigable
railroad diagrams of ChaiScript's syntax. The grammar was validated
against the parser implementation and covers all language constructs
including class inheritance, guard conditions, raw strings, and const
declarations that were missing from the original proposal. A reference
section was added to the cheatsheet, and a regression test exercises
every documented grammar construct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: add grammar railroad diagram link to README

Add a Grammar section to readme.md linking to the EBNF grammar file
and to mingodad's railroad diagram generator for direct viewing.

Requested by @lefticus in PR #673 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-14 10:59:48 -06:00 committed by GitHub
parent 0fd9cab654
commit 092ec417d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 415 additions and 0 deletions

View File

@ -915,3 +915,12 @@ set_print_handler(fun(s) { my_custom_log(s) })
## Extras ## Extras
ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.) ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.)
## Grammar Railroad Diagrams
A formal EBNF grammar for ChaiScript is available in [`grammar/chaiscript.ebnf`](grammar/chaiscript.ebnf). You can visualize it as navigable railroad diagrams by pasting its contents into one of these tools:
* [rr — Railroad Diagram Generator (IPv6)](https://www.bottlecaps.de/rr/ui)
* [rr — Railroad Diagram Generator (IPv4)](https://rr.red-dove.com/ui)
Open either link, switch to the **Edit Grammar** tab, paste the file contents, then click **View Diagram**.

177
grammar/chaiscript.ebnf Normal file
View File

@ -0,0 +1,177 @@
/*
* ChaiScript Grammar EBNF for Railroad Diagram Generation
*
* View as navigable railroad diagrams at:
* https://www.bottlecaps.de/rr/ui (IPv6)
* https://rr.red-dove.com/ui (IPv4)
*
* Copy and paste this file into the 'Edit Grammar' tab, then
* click 'View Diagram'.
*
* This grammar uses the notation accepted by
* https://github.com/GuntherRademacher/rr :
* - "::=" as rule separator
* - no semicolon at end of rule
* - "?" "+" "*" for repetition
* - C comments
*/
/* ---- Top-level ---- */
statements ::= ( def | try | if | while | class | for
| switch | return | break | continue
| equation | block | eol )+
/* ---- Functions ---- */
def ::= "def" id ( "::" id )? "(" decl_arg_list ")" eol*
( ":" guard )? eol* block
lambda ::= "fun" ( "[" id_arg_list "]" )? "(" decl_arg_list ")" eol* block
guard ::= operator
/* ---- Exception handling ---- */
try ::= "try" eol* block catch* finally?
catch ::= "catch" ( "(" arg ")" )? eol* block
finally ::= "finally" eol* block
/* ---- Control flow ---- */
if ::= "if" "(" equation ( eol equation )? ")" eol* block
( "else" ( if | eol* block ) )*
while ::= "while" "(" operator ")" eol* block
for ::= "for" "(" ( for_guards | equation ":" equation ) ")" eol* block
for_guards ::= equation eol equation eol equation
switch ::= "switch" "(" operator ")" eol* "{" ( case | default )+ "}"
case ::= "case" "(" operator ")" eol* block
default ::= "default" eol* block
/* ---- Classes ---- */
class ::= "class" id ( ":" id )? eol* class_block
class_block ::= "{" class_statements* "}"
class_statements ::= def | var_decl | eol
/* ---- Blocks & flow keywords ---- */
block ::= "{" statements* "}"
return ::= "return" operator?
break ::= "break"
continue ::= "continue"
/* ---- Line termination ---- */
eol ::= "\n" | "\r\n" | ";"
/* ---- Equations & operators ---- */
equation ::= operator ( ( "=" | ":=" | "+=" | "-=" | "*=" | "/="
| "%=" | "<<=" | ">>=" | "&=" | "^=" | "|=" )
equation )?
operator ::= prefix
| value
| operator binary_operator operator
| operator "?" operator ":" operator
prefix ::= ( "++" | "--" | "-" | "+" | "!" | "~" ) operator
binary_operator ::= "||" | "&&"
| "|" | "^" | "&"
| "==" | "!="
| "<" | "<=" | ">" | ">="
| "<<" | ">>"
| "+" | "-"
| "*" | "/" | "%"
/* ---- Values & access ---- */
value ::= var_decl | dot_fun_array | prefix
dot_fun_array ::= ( lambda | num | quoted_string
| single_quoted_string | raw_string
| paren_expression | inline_container
| id )
( fun_call | array_call | dot_access )*
fun_call ::= "(" arg_list ")"
array_call ::= "[" operator "]"
dot_access ::= "." id
/* ---- Variable declarations ---- */
var_decl ::= ( "auto" | "var" | "const" ) ( reference | id )
| "global" ( reference | id )
| "attr" id ( "::" id )?
reference ::= "&" id
/* ---- Parenthesised & inline containers ---- */
paren_expression ::= "(" operator ")"
inline_container ::= "[" container_arg_list "]"
container_arg_list ::= value_range
| map_pair ( "," map_pair )*
| operator ( "," operator )*
value_range ::= operator ".." operator
map_pair ::= operator ":" operator
/* ---- String literals ---- */
quoted_string ::= '"' ( char | escape | interpolation )* '"'
single_quoted_string ::= "'" ( char | escape ) "'"
raw_string ::= 'R"' delimiter? "(" char* ")" delimiter? '"'
delimiter ::= [a-zA-Z0-9_]+
interpolation ::= "${" equation "}"
/* ---- Escape sequences ---- */
escape ::= "\" ( "'" | '"' | "?" | "\" | "a" | "b"
| "f" | "n" | "r" | "t" | "v" | "$"
| "0"
| "x" hex_digit+
| "u" hex_digit hex_digit hex_digit hex_digit
| "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit
| octal_digit+ )
/* ---- Argument lists ---- */
id_arg_list ::= id ( "," id )*
decl_arg_list ::= ( arg ( "," arg )* )?
arg_list ::= ( equation ( "," equation )* )?
arg ::= id id?
/* ---- Identifiers ---- */
id ::= ( [a-zA-Z_] [a-zA-Z0-9_]* )
| ( "`" [^`]+ "`" )
| "true" | "false"
| "Infinity" | "NaN"
| "_"
| "__LINE__" | "__FILE__" | "__FUNC__" | "__CLASS__"
/* ---- Numeric literals ---- */
num ::= hex | binary | float | integer
hex ::= "0" ( "x" | "X" ) [0-9a-fA-F]+ int_suffix*
binary ::= "0" ( "b" | "B" ) [01]+ int_suffix*
float ::= [0-9]+ "." [0-9]+ ( ( "e" | "E" ) ( "+" | "-" )? [0-9]+ )? float_suffix?
integer ::= [0-9]+ int_suffix*
int_suffix ::= "l" | "L" | "ll" | "LL" | "u" | "U"
float_suffix ::= "l" | "L" | "f" | "F"
/* ---- Character classes ---- */
octal_digit ::= [0-7]
hex_digit ::= [0-9a-fA-F]
char ::= [^"\]

View File

@ -91,6 +91,16 @@ the doxygen documentation in the build folder or see the website
http://www.chaiscript.com. http://www.chaiscript.com.
Grammar
=======
A formal EBNF grammar for ChaiScript is available in
[grammar/chaiscript.ebnf](grammar/chaiscript.ebnf). To view it as a railroad
diagram, paste the grammar into
[mingodad's railroad diagram generator](https://mingodad.github.io/plgh/json2ebnf.html)
or [bottlecaps.de/rr](https://www.bottlecaps.de/rr/ui).
The shortest complete example possible follows: The shortest complete example possible follows:
```C++ ```C++

View File

@ -0,0 +1,219 @@
// Regression test: exercises grammar constructs documented in grammar/chaiscript.ebnf
// --- Variable declarations ---
var a = 1
auto b = 2
global c = 3
const d = 42
assert_equal(1, a)
assert_equal(2, b)
assert_equal(3, c)
assert_equal(42, d)
// --- Reference variables ---
var orig = 10
var &ref = orig
ref = 20
assert_equal(20, orig)
// --- Numeric literals ---
assert_equal(255, 0xFF)
assert_equal(255, 0xff)
assert_equal(5, 0b101)
assert_equal(42, 42)
assert_equal(3.14, 3.14)
// --- String interpolation ---
var name = "world"
assert_equal("hello world", "hello ${name}")
// --- Escape sequences ---
assert_equal("\n", "\n")
assert_equal("\t", "\t")
// --- Single-quoted char ---
assert_equal('A', 'A')
// --- Operators and precedence ---
assert_equal(7, 1 + 2 * 3)
assert_equal(true, 5 > 3 && 2 < 4)
assert_equal(true, false || true)
assert_equal(6, 3 << 1)
assert_equal(1, 3 >> 1)
assert_equal(5, 7 & 5)
assert_equal(7, 5 | 3)
assert_equal(6, 5 ^ 3)
assert_equal(-1, ~0)
// --- Ternary operator ---
assert_equal("yes", true ? "yes" : "no")
assert_equal("no", false ? "yes" : "no")
// --- Prefix operators ---
var x = 5
++x
assert_equal(6, x)
--x
assert_equal(5, x)
assert_equal(true, !false)
// --- Assignment operators ---
var v = 10
v += 5; assert_equal(15, v)
v -= 3; assert_equal(12, v)
v *= 2; assert_equal(24, v)
v /= 4; assert_equal(6, v)
v %= 4; assert_equal(2, v)
v <<= 2; assert_equal(8, v)
v >>= 1; assert_equal(4, v)
v |= 3; assert_equal(7, v)
v &= 5; assert_equal(5, v)
v ^= 3; assert_equal(6, v)
// --- Lambda ---
var add = fun(a, b) { a + b }
assert_equal(5, add(2, 3))
// --- Lambda with capture ---
var captured = 100
var get_captured = fun[captured]() { captured }
assert_equal(100, get_captured())
// --- Function definition ---
def multiply(a, b) { a * b }
assert_equal(12, multiply(3, 4))
// --- Guard condition on function ---
def abs_val(x) : x >= 0 { x }
def abs_val(x) : x < 0 { -x }
assert_equal(5, abs_val(5))
assert_equal(5, abs_val(-5))
// --- Class definition ---
class Animal
{
attr sound
def Animal(s) { this.sound = s }
def speak() { this.sound }
}
var dog = Animal("woof")
assert_equal("woof", dog.speak())
// --- Class with inheritance ---
class Puppy : Animal
{
attr name
def Puppy(n, s) { this.name = n; this.sound = s }
def greet() { to_string(this.name) + " says " + to_string(this.speak()) }
}
var p = Puppy("Rex", "yip")
assert_equal("Rex says yip", p.greet())
// --- Control flow: if/else ---
var result = ""
if (true) { result = "yes" } else { result = "no" }
assert_equal("yes", result)
// --- Control flow: while ---
var counter = 0
while (counter < 3) { ++counter }
assert_equal(3, counter)
// --- Control flow: for ---
var sum = 0
for (var i = 0; i < 5; ++i) { sum += i }
assert_equal(10, sum)
// --- Control flow: ranged for ---
var items = [10, 20, 30]
var total = 0
for (item : items) { total += item }
assert_equal(60, total)
// --- Switch/case ---
def classify(n) {
var label = ""
switch (n) {
case (1) { label = "one"; break }
case (2) { label = "two"; break }
default { label = "other" }
}
return label
}
assert_equal("one", classify(1))
assert_equal("two", classify(2))
assert_equal("other", classify(99))
// --- Try/catch/finally ---
var caught = false
var finalized = false
try {
throw("oops")
} catch (e) {
caught = true
} finally {
finalized = true
}
assert_true(caught)
assert_true(finalized)
// --- Inline containers ---
var vec = [1, 2, 3]
assert_equal(3, vec.size())
var m = ["a": 1, "b": 2]
assert_equal(1, m["a"])
var r = [1, 2, 3, 4, 5]
assert_equal(5, r.size())
// --- Dot access chaining ---
assert_equal(3, [1, 2, 3].size())
// --- Array access ---
var arr = [10, 20, 30]
assert_equal(20, arr[1])
// --- Backtick identifier ---
var `my var` = 42
assert_equal(42, `my var`)
// --- Special identifiers ---
assert_equal(true, true)
assert_equal(false, false)
// --- Nested block ---
var block_result = 0
{ block_result = 42 }
assert_equal(42, block_result)
// --- Break and continue ---
var break_sum = 0
for (var i = 0; i < 10; ++i) {
if (i == 5) { break }
break_sum += i
}
assert_equal(10, break_sum)
var cont_sum = 0
for (var i = 0; i < 5; ++i) {
if (i == 2) { continue }
cont_sum += i
}
assert_equal(8, cont_sum)
// --- Return from function ---
def early_return(n) {
if (n > 0) { return "positive" }
return "non-positive"
}
assert_equal("positive", early_return(1))
assert_equal("non-positive", early_return(-1))
// --- Colon assignment ---
var ca = 0
ca := 99
assert_equal(99, ca)