more grammar and some interpreter

This commit is contained in:
Norman Köhring 2025-06-17 15:39:38 +02:00
parent c40a6ef937
commit 40bf37dd77
13 changed files with 1765 additions and 734 deletions

View file

@ -1,79 +0,0 @@
YourLang {
// Entry point
Program = Statement*
// Statements
Statement = LiveDecl
| ComputedDecl
| TrackStmt
| VarDecl
| ExprStatement
| Block
// Reactive declarations
LiveDecl = "live" identifier "=" Expr
ComputedDecl = "computed" identifier Block
TrackStmt = "track" ListOf<identifier, ","> LambdaParams? Block
// Regular variable declarations
VarDecl = VarType identifier "=" Expr
VarType = "const" | "var" | "let"
// Expression statement (for function calls, etc)
ExprStatement = Expr
// Blocks and lambdas
Block = "{" Statement* "}"
LambdaParams = "|" ListOf<identifier, ","> "|"
// Expressions (precedence from loose to tight)
Expr = AssignExpr
AssignExpr = CompareExpr ("=" CompareExpr)?
CompareExpr = AddExpr (CompareOp AddExpr)?
CompareOp = "==" | "!=" | "<=" | ">=" | "<" | ">"
AddExpr = MulExpr (("+"|"-") MulExpr)*
MulExpr = UnaryExpr (("*"|"/"|"%") UnaryExpr)*
UnaryExpr = "!" UnaryExpr
| "-" UnaryExpr
| CallExpr
CallExpr = MemberExpr ("(" ListOf<Expr, ","> ")")?
MemberExpr = PrimaryExpr ("." identifier)*
PrimaryExpr = number
| string
| boolean
| identifier
| "(" Expr ")"
| Block // Block expressions
// Literals
number = digit+ ("." digit+)?
string = "\"" (~"\"" any)* "\""
| "'" (~"'" any)* "'"
| "`" templateChar* "`"
templateChar = ~"`" any
boolean = "true" | "false"
// Identifiers and keywords
identifier = ~keyword letter (letter | digit | "_")*
keyword = "live" | "computed" | "track" | "const" | "var" | "let"
| "true" | "false" | "if" | "else" | "for" | "while"
// Lexical rules
space += "//" (~"\n" any)* // Single-line comments
| "/*" (~"*/" any)* "*/" // Multi-line comments
}

View file

@ -17,10 +17,11 @@
"url": "https://git.koehr.ing/n/ohm-grammar-solace/issues"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest"
},
"packageManager": "pnpm@10.5.2",
"devDependencies": {
"ohm-js": "^17.1.0"
"ohm-js": "^17.1.0",
"vitest": "^3.2.3"
}
}

917
pnpm-lock.yaml generated
View file

@ -11,13 +11,930 @@ importers:
ohm-js:
specifier: ^17.1.0
version: 17.1.0
vitest:
specifier: ^3.2.3
version: 3.2.3
packages:
'@esbuild/aix-ppc64@0.25.5':
resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.5':
resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.5':
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.5':
resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.5':
resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.5':
resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.5':
resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.5':
resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.5':
resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.5':
resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.5':
resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.5':
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.5':
resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.5':
resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.5':
resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.5':
resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.5':
resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.5':
resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.5':
resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.5':
resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.5':
resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.25.5':
resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.5':
resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.5':
resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.5':
resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@rollup/rollup-android-arm-eabi@4.42.0':
resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.42.0':
resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.42.0':
resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.42.0':
resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.42.0':
resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.42.0':
resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.42.0':
resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.42.0':
resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.42.0':
resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.42.0':
resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.42.0':
resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.42.0':
resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.42.0':
resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.42.0':
resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.42.0':
resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==}
cpu: [x64]
os: [win32]
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@vitest/expect@3.2.3':
resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==}
'@vitest/mocker@3.2.3':
resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.3':
resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==}
'@vitest/runner@3.2.3':
resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==}
'@vitest/snapshot@3.2.3':
resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==}
'@vitest/spy@3.2.3':
resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==}
'@vitest/utils@3.2.3':
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
chai@5.2.0:
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
engines: {node: '>=12'}
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
esbuild@0.25.5:
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
engines: {node: '>=18'}
hasBin: true
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.2.1:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
ohm-js@17.1.0:
resolution: {integrity: sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==}
engines: {node: '>=0.12.1'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pathval@2.0.0:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
postcss@8.5.4:
resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.42.0:
resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.9.0:
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.14:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tinypool@1.1.0:
resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.3:
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
engines: {node: '>=14.0.0'}
vite-node@3.2.3:
resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@6.3.5:
resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitest@3.2.3:
resolution: {integrity: sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.3
'@vitest/ui': 3.2.3
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
snapshots:
'@esbuild/aix-ppc64@0.25.5':
optional: true
'@esbuild/android-arm64@0.25.5':
optional: true
'@esbuild/android-arm@0.25.5':
optional: true
'@esbuild/android-x64@0.25.5':
optional: true
'@esbuild/darwin-arm64@0.25.5':
optional: true
'@esbuild/darwin-x64@0.25.5':
optional: true
'@esbuild/freebsd-arm64@0.25.5':
optional: true
'@esbuild/freebsd-x64@0.25.5':
optional: true
'@esbuild/linux-arm64@0.25.5':
optional: true
'@esbuild/linux-arm@0.25.5':
optional: true
'@esbuild/linux-ia32@0.25.5':
optional: true
'@esbuild/linux-loong64@0.25.5':
optional: true
'@esbuild/linux-mips64el@0.25.5':
optional: true
'@esbuild/linux-ppc64@0.25.5':
optional: true
'@esbuild/linux-riscv64@0.25.5':
optional: true
'@esbuild/linux-s390x@0.25.5':
optional: true
'@esbuild/linux-x64@0.25.5':
optional: true
'@esbuild/netbsd-arm64@0.25.5':
optional: true
'@esbuild/netbsd-x64@0.25.5':
optional: true
'@esbuild/openbsd-arm64@0.25.5':
optional: true
'@esbuild/openbsd-x64@0.25.5':
optional: true
'@esbuild/sunos-x64@0.25.5':
optional: true
'@esbuild/win32-arm64@0.25.5':
optional: true
'@esbuild/win32-ia32@0.25.5':
optional: true
'@esbuild/win32-x64@0.25.5':
optional: true
'@jridgewell/sourcemap-codec@1.5.0': {}
'@rollup/rollup-android-arm-eabi@4.42.0':
optional: true
'@rollup/rollup-android-arm64@4.42.0':
optional: true
'@rollup/rollup-darwin-arm64@4.42.0':
optional: true
'@rollup/rollup-darwin-x64@4.42.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.42.0':
optional: true
'@rollup/rollup-freebsd-x64@4.42.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.42.0':
optional: true
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.42.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.42.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.42.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.42.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.42.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.42.0':
optional: true
'@types/chai@5.2.2':
dependencies:
'@types/deep-eql': 4.0.2
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.7': {}
'@types/estree@1.0.8': {}
'@vitest/expect@3.2.3':
dependencies:
'@types/chai': 5.2.2
'@vitest/spy': 3.2.3
'@vitest/utils': 3.2.3
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.3(vite@6.3.5)':
dependencies:
'@vitest/spy': 3.2.3
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5
'@vitest/pretty-format@3.2.3':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.3':
dependencies:
'@vitest/utils': 3.2.3
pathe: 2.0.3
strip-literal: 3.0.0
'@vitest/snapshot@3.2.3':
dependencies:
'@vitest/pretty-format': 3.2.3
magic-string: 0.30.17
pathe: 2.0.3
'@vitest/spy@3.2.3':
dependencies:
tinyspy: 4.0.3
'@vitest/utils@3.2.3':
dependencies:
'@vitest/pretty-format': 3.2.3
loupe: 3.1.3
tinyrainbow: 2.0.0
assertion-error@2.0.1: {}
cac@6.7.14: {}
chai@5.2.0:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
loupe: 3.1.3
pathval: 2.0.0
check-error@2.1.1: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
deep-eql@5.0.2: {}
es-module-lexer@1.7.0: {}
esbuild@0.25.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.5
'@esbuild/android-arm': 0.25.5
'@esbuild/android-arm64': 0.25.5
'@esbuild/android-x64': 0.25.5
'@esbuild/darwin-arm64': 0.25.5
'@esbuild/darwin-x64': 0.25.5
'@esbuild/freebsd-arm64': 0.25.5
'@esbuild/freebsd-x64': 0.25.5
'@esbuild/linux-arm': 0.25.5
'@esbuild/linux-arm64': 0.25.5
'@esbuild/linux-ia32': 0.25.5
'@esbuild/linux-loong64': 0.25.5
'@esbuild/linux-mips64el': 0.25.5
'@esbuild/linux-ppc64': 0.25.5
'@esbuild/linux-riscv64': 0.25.5
'@esbuild/linux-s390x': 0.25.5
'@esbuild/linux-x64': 0.25.5
'@esbuild/netbsd-arm64': 0.25.5
'@esbuild/netbsd-x64': 0.25.5
'@esbuild/openbsd-arm64': 0.25.5
'@esbuild/openbsd-x64': 0.25.5
'@esbuild/sunos-x64': 0.25.5
'@esbuild/win32-arm64': 0.25.5
'@esbuild/win32-ia32': 0.25.5
'@esbuild/win32-x64': 0.25.5
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
expect-type@1.2.1: {}
fdir@6.4.6(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fsevents@2.3.3:
optional: true
js-tokens@9.0.1: {}
loupe@3.1.3: {}
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
ms@2.1.3: {}
nanoid@3.3.11: {}
ohm-js@17.1.0: {}
pathe@2.0.3: {}
pathval@2.0.0: {}
picocolors@1.1.1: {}
picomatch@4.0.2: {}
postcss@8.5.4:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.42.0:
dependencies:
'@types/estree': 1.0.7
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.42.0
'@rollup/rollup-android-arm64': 4.42.0
'@rollup/rollup-darwin-arm64': 4.42.0
'@rollup/rollup-darwin-x64': 4.42.0
'@rollup/rollup-freebsd-arm64': 4.42.0
'@rollup/rollup-freebsd-x64': 4.42.0
'@rollup/rollup-linux-arm-gnueabihf': 4.42.0
'@rollup/rollup-linux-arm-musleabihf': 4.42.0
'@rollup/rollup-linux-arm64-gnu': 4.42.0
'@rollup/rollup-linux-arm64-musl': 4.42.0
'@rollup/rollup-linux-loongarch64-gnu': 4.42.0
'@rollup/rollup-linux-powerpc64le-gnu': 4.42.0
'@rollup/rollup-linux-riscv64-gnu': 4.42.0
'@rollup/rollup-linux-riscv64-musl': 4.42.0
'@rollup/rollup-linux-s390x-gnu': 4.42.0
'@rollup/rollup-linux-x64-gnu': 4.42.0
'@rollup/rollup-linux-x64-musl': 4.42.0
'@rollup/rollup-win32-arm64-msvc': 4.42.0
'@rollup/rollup-win32-ia32-msvc': 4.42.0
'@rollup/rollup-win32-x64-msvc': 4.42.0
fsevents: 2.3.3
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@3.9.0: {}
strip-literal@3.0.0:
dependencies:
js-tokens: 9.0.1
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
tinyglobby@0.2.14:
dependencies:
fdir: 6.4.6(picomatch@4.0.2)
picomatch: 4.0.2
tinypool@1.1.0: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.3: {}
vite-node@3.2.3:
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite@6.3.5:
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
picomatch: 4.0.2
postcss: 8.5.4
rollup: 4.42.0
tinyglobby: 0.2.14
optionalDependencies:
fsevents: 2.3.3
vitest@3.2.3:
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.3
'@vitest/mocker': 3.2.3(vite@6.3.5)
'@vitest/pretty-format': 3.2.3
'@vitest/runner': 3.2.3
'@vitest/snapshot': 3.2.3
'@vitest/spy': 3.2.3
'@vitest/utils': 3.2.3
chai: 5.2.0
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
picomatch: 4.0.2
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tinypool: 1.1.0
tinyrainbow: 2.0.0
vite: 6.3.5
vite-node: 3.2.3
why-is-node-running: 2.3.0
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2

88
src/compile.mjs Normal file
View file

@ -0,0 +1,88 @@
/* eslint-env node */
'use strict'
// --------------------------------------------------------------------
// Imports
// --------------------------------------------------------------------
import { readFileSync } from 'node:fs'
import * as solace from './interpreter.mjs'
// --------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------
function removeShebang(source) {
return source.slice(0, 2) === '#!' ? source.replace('#!', '//') : source
}
function parseArgs(args) {
const filenames = []
let benchmark = false
let verbose = false
for (let i = 0; i < args.length; ++i) {
const arg = args[i]
if (arg.startsWith('-')) { // flag, please?
if (arg === '-b' || arg === '--benchmark') benchmark = true
if (arg === '-v' || arg === '--verbose') verbose = true
continue
}
filenames.push(arg)
}
return { filenames, benchmark, verbose }
}
// --------------------------------------------------------------------
// Main
// --------------------------------------------------------------------
/* eslint-disable no-console */
function compile(args) {
const { filenames, benchmark, verbose } = parseArgs(args)
const files = filenames.map(name => {
// If benchmarking, read in all the files at once, so that we can just time the matching.
return [name, benchmark ? removeShebang(readFileSync(name).toString()) : null]
})
const matchStartTime = Date.now()
// Parsing -- bails out when the first error is encountered.
const results = []
const success = files.every(arr => {
const source = arr[1] ?? removeShebang(readFileSync(arr[0]).toString())
if (verbose) { console.error(arr[0]) }
const result = solace.grammar.match(source, 'Program')
if (!result.succeeded()) {
console.error(arr[0] + ':\n' + result.message)
return false
}
results.push(result)
return true
})
if (!success) return null
if (benchmark) console.error('Matching:', (Date.now() - matchStartTime) + 'ms')
// Codegen
const codegenStartTime = Date.now()
const code = results.map(r => solace.semantics(r).toES6()).join('\n\n')
if (benchmark) console.error('Codegen:', (Date.now() - codegenStartTime) + 'ms')
return code
}
// Run as program
if (import.meta && process.argv[1]) {
console.log(compile(process.argv.slice(2)))
}
// Imported as module
export default compile

169
src/grammar.ohm Normal file
View file

@ -0,0 +1,169 @@
Solace {
Program = Statement+
sourceCharacter = any
space := whitespace | lineTerminator | comment
whitespace = "\t"
| "\x0B" -- verticalTab
| "\x0C" -- formFeed
| " "
| "\u00A0" -- noBreakSpace
| "\uFEFF" -- byteOrderMark
| unicodeSpaceSeparator
lineTerminator = "\n" | "\r" | "\u2028" | "\u2029"
lineTerminatorSequence = "\n" | "\r" ~"\n" | "\u2028" | "\u2029" | "\r\n"
unicodeSpaceSeparator = "\u2000".."\u200B" | "\u3000"
comment = multiLineComment | singleLineComment
multiLineComment = "/*" (~"*/" sourceCharacter)* "*/"
singleLineComment = "//" (~lineTerminator sourceCharacter)*
Statement = FunctionDeclaration
| VariableDeclaration
| IfStatement
| ReturnStatement
| Block
| Expression
ReturnStatement = return Expression?
FunctionDeclaration = fn identifier "(" ListOf<Parameter, ","> ")" Block
FunctionExpression = fn? "(" ListOf<Parameter, ","> ")" "=>" Block -- arrow
| fn "(" ListOf<Parameter, ","> ")" Block -- anon
Parameter = identifier
Block = "{" Statement* "}"
IfStatement = if Expression Block (else (IfStatement | Block))?
VariableDeclaration = VarType identifier "=" Expression
VarType = const | var | live
Expression = AssignmentExpression
AssignmentExpression = LeftHandSide "=" AssignmentExpression -- assign
| ConditionalExpression
| FunctionExpression
ConditionalExpression = IfExpression
| LogicalORExpression
BitwiseANDExpression
= BitwiseANDExpression "&" ComparisonExpression -- band
| EqualityExpression
BitwiseXORExpression
= BitwiseXORExpression "^" BitwiseANDExpression -- bxor
| BitwiseANDExpression
BitwiseORExpression
= BitwiseORExpression "|" BitwiseXORExpression -- bor
| BitwiseXORExpression
LogicalANDExpression
= LogicalANDExpression "&&" BitwiseORExpression -- land
| BitwiseORExpression
LogicalORExpression
= LogicalORExpression "||" LogicalANDExpression -- lor
| LogicalANDExpression
TernaryExpression
= LogicalORExpression "?" AssignmentExpression ":" AssignmentExpression -- ternary
| LogicalORExpression
EqualityExpression = EqualityExpression "==" EqualityExpression -- eq
| EqualityExpression "===" EqualityExpression -- eeq
| EqualityExpression "is" EqualityExpression -- is
| EqualityExpression "!=" EqualityExpression -- ne
| ComparisonExpression
ComparisonExpression = AddExpression ">" AddExpression -- gt
| AddExpression "<" AddExpression -- lt
| AddExpression "<=" AddExpression -- le
| AddExpression ">=" AddExpression -- ge
| AddExpression
AddExpression = AddExpression "+" MultiplyExpression -- add
| AddExpression "-" MultiplyExpression -- subtract
| MultiplyExpression
MultiplyExpression = MultiplyExpression "*" PrimaryExpression -- multiply
| MultiplyExpression "/" PrimaryExpression -- divide
| MultiplyExpression "**" PrimaryExpression -- power
| MultiplyExpression "%" PrimaryExpression -- modulo
| UnaryExpression
UnaryExpression = "!" UnaryExpression -- not
| "-" UnaryExpression -- negate
| "+" UnaryExpression -- positive
| PostfixExpression
PostfixExpression = PostfixExpression "[" Expression "]" -- index
| PostfixExpression "." identifier -- property
| PostfixExpression "(" Arguments ")" -- call
| PrimaryExpression
PrimaryExpression = literal
| identifier
| ArrayLiteral
| StructLiteral
| DictLiteral
| "(" Expression ")" -- parentheses
LeftHandSide = PostfixExpression
IfExpression = if Expression Block else (IfExpression | Block)
Arguments = ListOf<Argument, ",">
Argument = Expression
ArrayLiteral = "[" ListOf<Expression, ","> "]"
StructLiteral = struct "{" "}"
DictLiteral = dict "{" "}"
literal = nullLiteral | undefined | booleanLiteral | numericLiteral | stringLiteral //TODO: | regExpLiteral
numericLiteral = ("-"|"+")? digit+ ("." digit+)? exponent?
exponent = ("e"|"E") ("-"|"+")? digit+
stringLiteral = "\"" (~"\"" sourceCharacter)* "\"" // "string"
| "'" (~"'" sourceCharacter)* "'" // 'string'
fn = "fn" ~identifierPart
return = "return" ~identifierPart
if = "if" ~identifierPart
else = "else" ~identifierPart
const = "const" ~identifierPart
var = "var" ~identifierPart
live = "live" ~identifierPart
true = "true" ~identifierPart
false = "false" ~identifierPart
null = "none" ~identifierPart
undefined = "undefined" ~identifierPart
struct = "struct" ~identifierPart
dict = "dict" ~identifierPart
identifier = ~reservedWord identifierName
identifierName = letter identifierPart*
identifierPart = letter | digit | "_" | "$"
reservedWord = keyword | reservedWord | nullLiteral | booleanLiteral | undefinedLiteral
keyword = fn | return | if | else
| const | var | live
| struct | dict
booleanLiteral = true | false
nullLiteral = null
undefinedLiteral = undefined
}

230
src/grammar.test.js Normal file
View file

@ -0,0 +1,230 @@
import fs from 'fs'
import { describe, it, expect, beforeAll } from 'vitest'
import { grammar as createGrammar } from 'ohm-js'
describe('Solace Grammar', () => {
let grammar
function check(input, shouldSucceed = true) {
const match = grammar.match(input)
const success = match.succeeded()
if (shouldSucceed && !success) {
console.log(`Failed to parse «${input}», error:`, match.message)
} else if (!shouldSucceed && success) {
console.log(`Invalid input successfully parsed: «${input}»`, match)
}
expect(success).toBe(shouldSucceed)
}
beforeAll(() => {
const grammarText = fs.readFileSync('./src/grammar.ohm', 'utf-8')
grammar = createGrammar(grammarText)
})
describe('Basic statements and expressions', () => {
it('literals', () => {
check('true')
check('false')
check('none')
check('undefined')
})
it('integers', () => {
check('42')
check('0')
check('1')
check('023')
check('9999')
})
it('integers with prefix', () => {
check('-1')
check('-23')
check('-0')
check('+0')
check('+999')
})
it('exponents', () => {
check('42e2') // 4200
check('1e+3') // 1000
check('2e-3') // 0.002
})
it('floats', () => {
check('1.0')
check('23.744')
check('-0.1')
check('+1.23')
check('999.000001')
})
it('strings in doublequotes', () => {
check('"Foo"')
check('"23"')
check('"Hello World"')
})
it('strings in singlequotes', () => {
check("'Foo'")
check("'23'")
check("'Hello World'")
})
it('arithmetic expressions', () => {
check("42 + 23")
check("42 - 23")
check("foo + 23 - 42e-2")
check("foo + bar")
check("42 - foo")
check("bar - foo")
check("bar-foo")
check("bar-42")
check("(42 - 23E2) + foo")
check("42**2 * foo")
check("(42 - 23) ** 2")
check("(42 % 23) / bar*2")
})
})
describe("Statements and Declarations", () => {
it('if statements', () => {
check(`if x > 5 { 10 }`)
check(`if foo < bar { 1 } else { 2 }`)
check(`
if foo < 23 {
"small"
} else if foo > 42 {
"large"
} else {
"medium"
}
`)
})
it('incomplete if statements', () => {
check(`
if > 23 {
"small"
}
`, false)
check(`
if foo < 23 {
"small"
} else if {
"wrong"
}
`, false)
})
it('basic functions and function calls', () => {
check(`
fn add(x,y) {
return x + y
}
fn foo() {
log(23)
}
add(2,3)
foo()
add(if answer is true { 19 } else { 0 }, 23)
`)
check(`fn () { return "wrong" }`) // anonymous function
})
it('variable declarations', () => {
check(`
var foo = 42
const bar = 'Hello'
live baz = (foo-1)*2
`)
})
it('arrays', () => {
check(`
const arr = ["Pirates", "know", "how", "to", "arr!"]
var ary = []
ary.push(23)
ary.push(5)
ary[1] = 42
`)
})
})
describe("Expressions", () => {
it('comparison', () => {
check(`x == 5`)
check(`x != 5`)
check(`x is true`)
check(`x <= 23`)
check(`x >= 23`)
check(`x > 23`)
check(`x < 23`)
})
it('logic', () => {
check(`x > 5 && !true && !(ary[0] > ary[1] || ary[0] == 5)`)
})
it('bitwise', () => {
check(`x & y | (2 ^ 5)`)
})
it('if expressions', () => {
check(`
const y = if x > 5 {
10
} else {
23
}
`)
check(`const y = if true { 10 } else { 23 }`)
// if expressions always need an else block
check(`
const y = if x > 5 {
10
}
`, false)
})
it('nested if expressions', () => {
check(`
const y = if x > 5 {
10
} else if x < 2 {
0
} else {
2
}
`)
// if expressions always need an else block
check(`
const y = if x > 5 {
10
} else if x < 2 {
0
}
`, false)
})
it('function expressions', () => {
check(`const foo = fn () { return 42 }`) // anonymous function with fn
check(`const bar = fn () => { return 3e2 }`) // anon func with fn and arrow
check(`const baz = () => { return 3e2 }`) // anon arrow func
})
})
describe("Example Program", () => {
it('hello world', () => {
check(`
fn main() {
const ary = ["foo", "bar"]
ary[1] = 'World'
console.log('Hello', ary[1])
}
`)
})
})
})

165
src/interpreter.mjs Normal file
View file

@ -0,0 +1,165 @@
import fs from 'fs'
import { grammar as createGrammar } from 'ohm-js'
function flattenIterNodes(nodes) {
const result = []
for (let i = 0; i < nodes.length; ++i) {
if (nodes[i]._node.ctorName === '_iter') {
result.push(...flattenIterNodes(nodes[i].children))
} else {
result.push(nodes[i])
}
}
return result
}
// Comparison function for sorting nodes based on their interval's start index.
function compareByInterval(node, otherNode) {
return node.source.startIdx - otherNode.source.startIdx
}
function nodeToES6(node, children) {
const flatChildren = flattenIterNodes(children).sort(compareByInterval)
// Keeps track of where the previous sibling ended, so that we can re-insert discarded
// whitespace into the final output.
let prevEndIdx = node.source.startIdx
let code = ''
for (let i = 0; i < flatChildren.length; ++i) {
const child = flatChildren[i]
// Restore any discarded whitespace between this node and the previous one.
if (child.source.startIdx > prevEndIdx) {
code += node.source.sourceString.slice(prevEndIdx, child.source.startIdx)
}
code += child.toES6()
prevEndIdx = child.source.endIdx
}
return code
}
const grammarPath = `${import.meta.dirname}/grammar.ohm`
const grammarText = fs.readFileSync(grammarPath, 'utf-8')
const grammar = createGrammar(grammarText)
const semantics = grammar.createSemantics()
semantics.addOperation('toES6', {
Program(sourceElements) {
const { sourceString, startIdx, endIdx } = this.source
return (
sourceString.slice(0, startIdx) +
nodeToES6(this, [sourceElements]) +
sourceString.slice(endIdx)
)
},
VariableDeclaration(varType, ident, _equals, expr) {
const keyword = varType.sourceString
const name = ident.toES6()
const value = expr.toES6()
switch (keyword) {
case 'var': return `let ${name} = ${value}`
case 'live': return `let ${name} = ${value} /* TODO: reactivity not yet implemented */`
default: return `${keyword} ${name} = ${value}`
}
},
IfStatement(_ifKw, expr, thenBlock, _elseKw, elseBlock) {
const ifStmt = `if (${expr.toES6()}) ${thenBlock.toES6()}`
const elseStmt = `${elseBlock.toES6()}`.trim()
let output = ifStmt
if (elseStmt.length) output += ` else ${elseStmt}`
return output
},
EqualityExpression_eq(leftExpr, _eq, rightExpr) {
return `${leftExpr.toES6()} === ${rightExpr.toES6()}`
},
IfExpression(_ifKw, expr, thenBlock, _elseKw, elseBlock) {
const thenValue = thenBlock.blockAsExpression()
const elseValue = elseBlock.blockAsExpression()
return `(${expr.toES6()} ? ${thenValue} : ${elseValue})`
},
FunctionDeclaration(_fnKw, ident, _leftParen, params, _rightParen, block) {
return `const ${ident.toES6()} = (${params.toES6()}) => ${block.toES6()}`
},
FunctionExpression_arrow(_maybeFnKw, _leftParen, params, _rightParen, _arrow, block) {
return `(${params.toES6()}) => ${block.toES6()}`
},
FunctionExpression_anon(_fnKw, _leftParen, params, _rightParen, block) {
return `(${params.toES6()}) => ${block.toES6()}`
},
_nonterminal(...children) {
return nodeToES6(this, children)
},
_terminal() {
return this.sourceString
},
_iter(...children) {
return children.map(c => c.toES6()).join('')
},
_default(...children) {
// Reconstruct source from children
return children.map(child => {
if (typeof child.toES6 === 'function') return child.toES6()
return '' // Skip iteration nodes
}).join(' ')
},
})
// Blocks can be simple statements (if condition {/*block*/})
// or expressions (const foo = condition ? /*block*/ : /*block*/)
// which needs to return a value, so it is transpiled to an IIFE
// like (const foo = cond ? (() => { const x = 5; return x; })() : ... )
semantics.addOperation('blockAsExpression', {
Block(_open, statementList, _close) {
const statements = statementList.children
if (statements.length === 0) return ''
if (statements.length === 1) return statements[0].toES6()
const transpiled = statements.map(stmt => stmt.toES6())
const last = transpiled[transpiled.length - 1]
const init = transpiled.slice(0, -1).join('; ')
return `(() => { ${init}; return ${last}; })()`
},
IfExpression(_ifKw, expr, thenBlock, _elseKw, elseBlock) {
const thenValue = thenBlock.blockAsExpression()
const elseValue = elseBlock.blockAsExpression()
return `(${expr.toES6()} ? ${thenValue} : ${elseValue})`
},
})
semantics.addOperation('hoistDeclarations', {
FunctionDeclaration(_fnKw, ident, _leftParen, _params, _rightParen, _block) {
return new Map([[ident.sourceString, [ident.source]]])
},
FunctionExpression(_) {
return new Map()
},
_iter: mergeBindings,
_nonterminal: mergeBindings,
_terminal() {
return new Map()
},
})
// Merge the bindings from the given `nodes` into a single map, where the value
// is an array of source locations that name is bound.
function mergeBindings(...nodes) {
const bindings = new Map()
for (const child of nodes.filter(c => !c.isLexical())) {
child.hoistDeclarations().forEach((sources, ident) => {
if (bindings.has(ident)) {
bindings.get(ident).push(...sources) // Shadowed binding.
} else {
bindings.set(ident, sources) // Not shadowed at this level.
}
})
}
return bindings
}
export { grammar, semantics }

150
src/interpreter.test.js Normal file
View file

@ -0,0 +1,150 @@
import { describe, it, expect, beforeAll } from 'vitest'
import { grammar, semantics } from './interpreter.mjs'
function compile(source) {
const match = grammar.match(source, 'Program')
if (!match.succeeded()) {
console.error(`Failed to parse «${source}», error:`, match.message)
return null
}
return semantics(match).toES6()
}
describe('Solace Interpreter', () => {
describe('Expressions', () => {
it('addition/substraction', () => {
expect(compile('a + b - c')).toBe('a + b - c')
})
it('multiplication/division', () => {
expect(compile('a * b / c')).toBe('a * b / c')
})
it('mixed', () => {
expect(compile('a + b * c')).toBe('a + b * c')
})
it('others', () => {
expect(compile('a ** b % c')).toBe('a ** b % c')
})
it('negative', () => {
expect(compile('-2 * c')).toBe('-2 * c')
})
})
describe('Functions', () => {
it('one-liner declaration', () => {
const input = `fn foo() {}`
const output = compile(input)
expect(output).toBe('const foo = () => {}')
})
it('one-liner expression with fn', () => {
const input = `const foo = fn () {}`
const output = compile(input)
expect(output).toBe('const foo = () => {}')
})
it('one-liner expression with arrow', () => {
const input = `const foo = () => {}`
const output = compile(input)
expect(output).toBe('const foo = () => {}')
})
it('one-liner expression with fn and arrow', () => {
const input = `const foo = fn () => {}`
const output = compile(input)
expect(output).toBe('const foo = () => {}')
})
it('multi-line declaration', () => {
const input = `fn foo() {
const x = 5
return x
}`
const output = compile(input)
expect(output).toBe(`const foo = () => {
const x = 5
return x
}`)
})
})
describe('Assignments', () => {
it('var', () => {
const input = 'var foo = 23'
const output = compile(input)
expect(output).toBe('let foo = 23')
})
it('let', () => {
const input = 'let foo = 23'
const output = compile(input)
expect(output).toBe('let foo = 23')
})
it('const', () => {
const input = 'const foo = 23'
const output = compile(input)
expect(output).toBe('const foo = 23')
})
it('array declaration', () => {
const input = 'const foo = [1,2,3]'
const output = compile(input)
expect(output).toBe('const foo = [1,2,3]')
})
it('array usage', () => {
const input = 'const foo = ary[2]'
const output = compile(input)
expect(output).toBe('const foo = ary[2]')
})
})
describe('Conditions and Comparisons', () => {
it('if', () => {
const input = 'if x < 5 { console.log(x) }'
const output = compile(input)
expect(output).toBe('if (x < 5) { console.log(x) }')
})
it('if-else', () => {
const input = 'if x < 5 { console.log(x) } else { console.log("no") }'
const output = compile(input)
expect(output).toBe('if (x < 5) { console.log(x) } else { console.log("no") }')
})
it('nested if', () => {
const input = `if x < 5 {
console.log('small')
} else if x < 23 {
console.log('medium')
} else {
console.log('large')
}
`
const output = compile(input)
expect(output).toBe(`if (x < 5) {
console.log('small')
} else if (x < 23) {
console.log('medium')
} else {
console.log('large')
}`)
})
it('if expressions with single statements', () => {
const input = 'const foo = if x < 5 { "small" } else { "medium" }'
const output = compile(input)
expect(output).toBe('const foo = (x < 5 ? "small" : "medium")')
})
it('if expressions with multiple statements', () => {
const input = `const foo = if x < 5 {
const y = x*3
y
} else {
const y = x/3
console.log(y)
y
}`
const output = compile(input)
expect(output).toBe(`const foo = (x < 5 ? (() => { const y = x*3; return y; })() : (() => { const y = x/3; console.log(y); return y; })())`)
})
it('if expressions with mixed statements', () => {
const input = `const foo = if x < 5 {
const y = x*3
y
} else {
42
}`
const output = compile(input)
expect(output).toBe(`const foo = (x < 5 ? (() => { const y = x*3; return y; })() : 42)`)
})
})
})

43
src/test.solace Normal file
View file

@ -0,0 +1,43 @@
fn relief(array, index) {
if array.length > index {
const word = 'Solace'
array[index] = word
}
return array
}
fn doStuff(v) {
const result = if v === 5 {
const x = 5
x
} else if v == 23 {
const word = "World"
word
} else {
"Whatever"
}
/* Loop constructs are not supported, yet
*
* for (var i = 0; i < 23; i++) {
* console.log('i is now', i, 'so that we test loops')
* }
*/
return result
}
// function expressions
const func1 = fn(a,b) {}
const func2 = fn(a,b) { console.log(a,b) }
const func3 = fn(a,b) => { console.log(a,b) }
const func4 = () => {}
const func5 = (a,b) => { console.log(a,b) }
fn main() {
const ary = ["Hello", "World"]
const fooFun = fn () { "foo" }
live index = 1
relief(ary, index)
console.log(ary.join(' '))
}

123
stwl.js
View file

@ -1,123 +0,0 @@
/* eslint-env node */
'use strict';
// --------------------------------------------------------------------
// Imports
// --------------------------------------------------------------------
const fs = require('fs');
const path = require('path');
const ohm = require('ohm-js');
// --------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------
// Take an Array of nodes, and whenever an _iter node is encountered, splice in its
// recursively-flattened children instead.
function flattenIterNodes(nodes) {
const result = [];
for (let i = 0; i < nodes.length; ++i) {
if (nodes[i]._node.ctorName === '_iter') {
result.push(...flattenIterNodes(nodes[i].children));
} else {
result.push(nodes[i]);
}
}
return result;
}
// Comparison function for sorting nodes based on their interval's start index.
function compareByInterval(node, otherNode) {
return node.source.startIdx - otherNode.source.startIdx;
}
function nodeToES5(node, children) {
const flatChildren = flattenIterNodes(children).sort(compareByInterval);
// Keeps track of where the previous sibling ended, so that we can re-insert discarded
// whitespace into the final output.
let prevEndIdx = node.source.startIdx;
let code = '';
for (let i = 0; i < flatChildren.length; ++i) {
const child = flatChildren[i];
// Restore any discarded whitespace between this node and the previous one.
if (child.source.startIdx > prevEndIdx) {
code += node.source.sourceString.slice(prevEndIdx, child.source.startIdx);
}
code += child.toES5();
prevEndIdx = child.source.endIdx;
}
return code;
}
// Instantiate the ES5 grammar.
const contents = fs.readFileSync(path.join(__dirname, 'es5.ohm'));
const g = ohm.grammars(contents).ES5;
const semantics = g.createSemantics();
semantics.addOperation('toES5()', {
Program(_, sourceElements) {
// Top-level leading and trailing whitespace is not handled by nodeToES5(), so do it here.
const {sourceString} = this.source;
return (
sourceString.slice(0, this.source.startIdx) +
nodeToES5(this, [sourceElements]) +
sourceString.slice(this.source.endIdx)
);
},
_nonterminal(...children) {
return nodeToES5(this, children);
},
_terminal() {
return this.sourceString;
},
});
// Implements hoisting of variable and function declarations.
// See https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
// Note that on its own, this operation doesn't create nested lexical environments,
// but it should be possible to use it as a helper for another operation that would.
semantics.addOperation('hoistDeclarations()', {
FunctionDeclaration(_, ident, _1, _2, _3, _4, _5, _6) {
// Don't hoist from the function body, only return this function's identifier.
return new Map([[ident.sourceString, [ident.source]]]);
},
FunctionExpression(_) {
return new Map();
},
VariableDeclaration(ident, _) {
return new Map([[ident.sourceString, [ident.source]]]);
},
_iter: mergeBindings,
_nonterminal: mergeBindings,
_terminal() {
return new Map();
},
});
// Merge the bindings from the given `nodes` into a single map, where the value
// is an array of source locations that name is bound.
function mergeBindings(...nodes) {
const bindings = new Map();
for (const child of nodes.filter(c => !c.isLexical())) {
child.hoistDeclarations().forEach((sources, ident) => {
if (bindings.has(ident)) {
bindings.get(ident).push(...sources); // Shadowed binding.
} else {
bindings.set(ident, sources); // Not shadowed at this level.
}
});
}
return bindings;
}
module.exports = {
grammar: g,
semantics,
};

424
stwl.ohm
View file

@ -1,424 +0,0 @@
STWL {
Program = &(Directive*) SourceElement*
/*
STWL - Strongly Typed Web Language is just a working title
This grammar is based on the ES5 and ES6 grammars and extends/cuts it where
necessary to clear STWL from many, in my opinion unnecessary, features
while extending it with a clearer syntax
*/
sourceCharacter = any
// Override Ohm's built-in definition of space.
space := whitespace | lineTerminator | comment
whitespace = "\t"
| "\x0B" -- verticalTab
| "\x0C" -- formFeed
| " "
| "\u00A0" -- noBreakSpace
| "\uFEFF" -- byteOrderMark
| unicodeSpaceSeparator
lineTerminator = "\n" | "\r" | "\u2028" | "\u2029"
lineTerminatorSequence = "\n" | "\r" ~"\n" | "\u2028" | "\u2029" | "\r\n"
comment = multiLineComment | singleLineComment
multiLineComment = "/*" (~"*/" sourceCharacter)* "*/"
singleLineComment = "//" (~lineTerminator sourceCharacter)*
identifier (an identifier) = ~reservedWord identifierName
identifierName = identifierStart identifierPart*
identifierStart = letter | "$" | "_"
identifierPart = identifierStart | unicodeDigit
letter += unicodeCategoryNl
unicodeCategoryNl
= "\u2160".."\u2182" | "\u3007" | "\u3021".."\u3029"
unicodeDigit (a digit)
= "\u0030".."\u0039" | "\u0660".."\u0669" | "\u06F0".."\u06F9" | "\u0966".."\u096F" | "\u09E6".."\u09EF" | "\u0A66".."\u0A6F" | "\u0AE6".."\u0AEF" | "\u0B66".."\u0B6F" | "\u0BE7".."\u0BEF" | "\u0C66".."\u0C6F" | "\u0CE6".."\u0CEF" | "\u0D66".."\u0D6F" | "\u0E50".."\u0E59" | "\u0ED0".."\u0ED9" | "\u0F20".."\u0F29" | "\uFF10".."\uFF19"
unicodeSpaceSeparator = "\u2000".."\u200B" | "\u3000"
reservedWord = keyword | futureReservedWord | nullLiteral | booleanLiteral
// Note: keywords that are the complete prefix of another keyword should
// be prioritized (e.g. 'in' should come before 'instanceof')
keyword = for | while | do | break | continue | match
| if | else | const | var | live | computed
| catch | fn | return | void | this
| has | enum | interface | extends | implements | struct
| export | import | defer
futureReservedWord = fallthrough
literal = nullLiteral | booleanLiteral | numericLiteral
| stringLiteral | regularExpressionLiteral
// Optionals, which can be either Some<T> or None, are translated to T | null
nullLiteral = "None" ~identifierPart
booleanLiteral = ("true" | "false") ~identifierPart
// For semantics on how decimal literals are constructed, see section 7.8.3
// Note that the ordering of hexIntegerLiteral and decimalLiteral is reversed w.r.t. the spec
// This is intentional: the order decimalLiteral | hexIntegerLiteral will parse
// "0x..." as a decimal literal "0" followed by "x..."
numericLiteral = octalIntegerLiteral | hexIntegerLiteral | decimalLiteral
decimalLiteral = decimalIntegerLiteral "." decimalDigit* exponentPart -- bothParts
| "." decimalDigit+ exponentPart -- decimalsOnly
| decimalIntegerLiteral exponentPart -- integerOnly
decimalIntegerLiteral = nonZeroDigit decimalDigit* -- nonZero
| "0" -- zero
decimalDigit = "0".."9"
nonZeroDigit = "1".."9"
exponentPart = exponentIndicator signedInteger -- present
| -- absent
exponentIndicator = "e" | "E"
signedInteger = "+" decimalDigit* -- positive
| "-" decimalDigit* -- negative
| decimalDigit+ -- noSign
hexIntegerLiteral = "0x" hexDigit+
| "0X" hexDigit+
// hexDigit defined in Ohm's built-in rules (otherwise: hexDigit = "0".."9" | "a".."f" | "A".."F")
octalIntegerLiteral = "0" octalDigit+
octalDigit = "0".."7"
zeroToThree = "0".."3"
fourToSeven = "4".."7"
// For semantics on how string literals are constructed, see section 7.8.4
stringLiteral = "\"" doubleStringCharacter* "\""
| "'" singleStringCharacter* "'"
doubleStringCharacter = ~("\"" | "\\" | lineTerminator) sourceCharacter -- nonEscaped
| "\\" escapeSequence -- escaped
| lineContinuation -- lineContinuation
singleStringCharacter = ~("'" | "\\" | lineTerminator) sourceCharacter -- nonEscaped
| "\\" escapeSequence -- escaped
| lineContinuation -- lineContinuation
lineContinuation = "\\" lineTerminatorSequence
escapeSequence = unicodeEscapeSequence
| hexEscapeSequence
| octalEscapeSequence
| characterEscapeSequence // Must come last.
characterEscapeSequence = singleEscapeCharacter
| nonEscapeCharacter
singleEscapeCharacter = "'" | "\"" | "\\" | "b" | "f" | "n" | "r" | "t" | "v"
nonEscapeCharacter = ~(escapeCharacter | lineTerminator) sourceCharacter
escapeCharacter = singleEscapeCharacter | decimalDigit | "x" | "u"
octalEscapeSequence = zeroToThree octalDigit octalDigit -- whole
| fourToSeven octalDigit -- eightTimesfourToSeven
| zeroToThree octalDigit ~decimalDigit -- eightTimesZeroToThree
| octalDigit ~decimalDigit -- octal
hexEscapeSequence = "x" hexDigit hexDigit
unicodeEscapeSequence = "u" hexDigit hexDigit hexDigit hexDigit
// §7.8.5 Regular Expression Literals -- https://es5.github.io/#x7.8.5
regularExpressionLiteral = "/" regularExpressionBody "/" regularExpressionFlags
regularExpressionBody = regularExpressionFirstChar regularExpressionChar*
regularExpressionFirstChar = ~("*" | "\\" | "/" | "[") regularExpressionNonTerminator
| regularExpressionBackslashSequence
| regularExpressionClass
regularExpressionChar = ~("\\" | "/" | "[") regularExpressionNonTerminator
| regularExpressionBackslashSequence
| regularExpressionClass
regularExpressionBackslashSequence = "\\" regularExpressionNonTerminator
regularExpressionNonTerminator = ~(lineTerminator) sourceCharacter
regularExpressionClass = "[" regularExpressionClassChar* "]"
regularExpressionClassChar = ~("]" | "\\") regularExpressionNonTerminator
| regularExpressionBackslashSequence
regularExpressionFlags = identifierPart*
// === Implementation-level rules (not part of the spec) ===
multiLineCommentNoNL = "/*" (~("*/" | lineTerminator) sourceCharacter)* "*/"
// does not accept lineTerminators, not even implicit ones in a multiLineComment (cf. section 7.4)
spacesNoNL = (whitespace | singleLineComment | multiLineCommentNoNL)*
// A semicolon is "automatically inserted" if a newline or the end of the input stream is
// reached, or the offending token is "}".
// See https://es5.github.io/#x7.9 for more information.
// NOTE: Applications of this rule *must* appear in a lexical context -- either in the body of a
// lexical rule, or inside `#()`.
sc = space* (";" | end)
| spacesNoNL (lineTerminator | ~multiLineCommentNoNL multiLineComment | &"}")
// Convenience rules for parsing keyword tokens.
for = "for" ~identifierPart
while = "while" ~identifierPart
do = "do" ~identifierPart // will we support do-while loops?
break = "break" ~identifierPart
continue = "continue" ~identifierPart
if = "if" ~identifierPart
else = "else" ~identifierPart
const = "const" ~identifierPart
var = "var" ~identifierPart
live = "live" ~identifierPart
track = "track" ~identifierPart
computed = "computed" ~identifierPart
catch = "catch" ~identifierPart
fn = "fn" ~identifierPart
return = "return" ~identifierPart
void = "void" ~identifierPart
this = "_" ~identifierPart
in = "in" ~identifierPart -- value in ArrayLike
has = "has" ~identifierPart -- struct has Property
enum = "enum" ~identifierPart
interface = "interface" ~identifierPart
extends = "extends" ~identifierPart
implements = "implements" ~identifierPart
export = "export" ~identifierPart
import = "import" ~identifierPart
struct = "struct" ~identifierPart
defer = "defer" ~identifierPart
match = "match" ~identifierPart
fallthrough = "fallthrough" ~identifierPart
// end of lexical rules
noIn = ~in
withIn =
noHas = ~has
withHas =
noInHas = ~(in | has)
withInHas =
// §A.3 Expressions -- https://es5.github.io/#A.3
PrimaryExpression = this
| identifier
| literal
// ( litToken.type === "regexp"
// ? this.ast(_fromIdx, "RegExpExpr",{body: litToken.value.body
// flags: litToken.value.flags}, [])
// : this.ast(_fromIdx, "LiteralExpr",{type: litToken.type
// value: litToken.value}, []) )
| ArrayLiteral
| ObjectLiteral
| "(" Expression<withIn> ")" -- parenExpr
ArrayLiteral = "[" ListOf<AssignmentExpressionOrElision, ","> "]"
AssignmentExpressionOrElision = AssignmentExpression<withIn>
| -- elision
ObjectLiteral = "{" ListOf<PropertyAssignment, ","> "}" -- noTrailingComma
| "{" NonemptyListOf<PropertyAssignment, ","> "," "}" -- trailingComma
PropertyAssignment = get PropertyName "(" ")" "{" FunctionBody "}" -- getter
| set PropertyName "(" FormalParameter ")" "{" FunctionBody "}" -- setter
| PropertyName ":" AssignmentExpression<withIn> -- simple
PropertyName = identifierName
| stringLiteral
| numericLiteral
MemberExpression = MemberExpression "[" Expression<withIn> "]" -- arrayRefExp
| MemberExpression "." identifierName -- propRefExp
| new MemberExpression Arguments -- newExp
| FunctionExpression
| PrimaryExpression
NewExpression = MemberExpression
| new NewExpression -- newExp
CallExpression = CallExpression "[" Expression<withIn> "]" -- arrayRefExp
| CallExpression "." identifierName -- propRefExp
| CallExpression Arguments -- callExpExp
| MemberExpression Arguments -- memberExpExp
Arguments = "(" ListOf<AssignmentExpression<withIn>, ","> ")"
LeftHandSideExpression = CallExpression
| NewExpression
PostfixExpression = LeftHandSideExpression #(spacesNoNL "++") -- postIncrement
| LeftHandSideExpression #(spacesNoNL "--") -- postDecrement
| LeftHandSideExpression
UnaryExpression = void UnaryExpression -- voidExp
| "++" UnaryExpression -- preIncrement
| "--" UnaryExpression -- preDecrement
| "+" UnaryExpression -- unaryPlus
| "-" UnaryExpression -- unaryMinus
| "~" UnaryExpression -- bnot
| "!" UnaryExpression -- lnot
| PostfixExpression
MultiplicativeExpression = MultiplicativeExpression "*" UnaryExpression -- mul
| MultiplicativeExpression "/" UnaryExpression -- div
| MultiplicativeExpression "%" UnaryExpression -- mod
| UnaryExpression
AdditiveExpression = AdditiveExpression "+" MultiplicativeExpression -- add
| AdditiveExpression "-" MultiplicativeExpression -- sub
| MultiplicativeExpression
ShiftExpression = ShiftExpression "<<" AdditiveExpression -- lsl
| ShiftExpression ">>>" AdditiveExpression -- lsr
| ShiftExpression ">>" AdditiveExpression -- asr
| AdditiveExpression
RelationalExpression<guardIn>
= RelationalExpression<guardIn> "<" ShiftExpression -- lt
| RelationalExpression<guardIn> ">" ShiftExpression -- gt
| RelationalExpression<guardIn> "<=" ShiftExpression -- le
| RelationalExpression<guardIn> ">=" ShiftExpression -- ge
| RelationalExpression<guardIn> guardIn "in" ShiftExpression -- inExp
| ShiftExpression
EqualityExpression<guardIn>
= EqualityExpression<guardIn> "==" RelationalExpression<guardIn> -- equal
| EqualityExpression<guardIn> "!=" RelationalExpression<guardIn> -- notEqual
| EqualityExpression<guardIn> "===" RelationalExpression<guardIn> -- eq
| EqualityExpression<guardIn> "!==" RelationalExpression<guardIn> -- notEq
| RelationalExpression<guardIn>
BitwiseANDExpression<guardIn>
= BitwiseANDExpression<guardIn> "&" EqualityExpression<guardIn> -- band
| EqualityExpression<guardIn>
BitwiseXORExpression<guardIn>
= BitwiseXORExpression<guardIn> "^" BitwiseANDExpression<guardIn> -- bxor
| BitwiseANDExpression<guardIn>
BitwiseORExpression<guardIn>
= BitwiseORExpression<guardIn> "|" BitwiseXORExpression<guardIn> -- bor
| BitwiseXORExpression<guardIn>
LogicalANDExpression<guardIn>
= LogicalANDExpression<guardIn> "&&" BitwiseORExpression<guardIn> -- land
| BitwiseORExpression<guardIn>
LogicalORExpression<guardIn>
= LogicalORExpression<guardIn> "||" LogicalANDExpression<guardIn> -- lor
| LogicalANDExpression<guardIn>
ConditionalExpression<guardIn>
= LogicalORExpression<guardIn> "?" AssignmentExpression<withIn> ":" AssignmentExpression<guardIn> -- conditional
| LogicalORExpression<guardIn>
AssignmentExpression<guardIn>
= LeftHandSideExpression assignmentOperator AssignmentExpression<guardIn> -- assignment
| ConditionalExpression<guardIn>
Expression<guardIn> (an expression)
= Expression<guardIn> "," AssignmentExpression<guardIn> -- commaExp
| AssignmentExpression<guardIn>
assignmentOperator = "=" | ">>>=" | "<<=" | ">>="
| "*=" | "/=" | "%=" | "+=" | "-=" | "&=" | "^=" | "|="
// Statements -- (extends https://es5.github.io/#A.4)
Statement
= Block
| VariableStatement
| EmptyStatement
| ExpressionStatement
| IfStatement
| IterationStatement
| ContinueStatement
| BreakStatement
| ReturnStatement
Block = "{" StatementList "}"
LambdaParameters = "|" ListOf<identifier, ","> "|"
StatementList = Statement*
ComputedStatement = computed identifier Block
TrackStatement = track ListOf<identifier, ","> LambdaParameters? Block
VariableStatement = VariableAssignment VariableDeclarationList<withIn> #sc
VariableAssignment = var | const | live
VariableDeclarationList<guardIn> = NonemptyListOf<VariableDeclaration<guardIn>, ",">
VariableDeclaration<guardIn> = identifier Initialiser<guardIn>?
Initialiser<guardIn> = "=" AssignmentExpression<guardIn>
EmptyStatement = ";" // note: this semicolon eats newlines
ExpressionStatement = ~("{" | function) Expression<withIn> #sc
IfStatement = if "(" Expression<withIn> ")" Statement (else Statement)?
IterationStatement = do Statement while "(" Expression<withIn> ")" #sc -- doWhile
| while "(" Expression<withIn> ")" Statement -- whileDo
| for "(" Expression<noIn>? ";"
Expression<withIn>? ";"
Expression<withIn>? ")" Statement -- for3
| for "(" var VariableDeclarationList<noIn> ";"
Expression<withIn>? ";"
Expression<withIn>? ")" Statement -- for3var
| for "(" LeftHandSideExpression in
Expression<withIn> ")" Statement -- forIn
| for "(" var VariableDeclaration<noIn> in
Expression<withIn> ")" Statement -- forInVar
ContinueStatement = continue #((spacesNoNL identifier)? sc)
BreakStatement = break #((spacesNoNL identifier)? sc)
ReturnStatement = return (#(spacesNoNL ~space) Expression<withIn>)? #sc
Catch = catch "(" FormalParameter ")" Block
Defer = defer (when Expression<withIn>)? Block
// Pattern Matching
MatchExpr = match Expression<withIn> (if Pattern)? "{" MatchArm+ "}" (else Block)?
MatchArm = Pattern "=>" (Expression<withIn> | Block)
Pattern = identifier
| literal
| GuardPattern
GuardPattern = Expression<withIn> // Just an expression! No special syntax needed
// §A.5 Functions and Programs -- https://es5.github.io/#A.5
FunctionDeclaration
= function identifier "(" FormalParameterList ")" "{" FunctionBody "}"
FunctionExpression
= function identifier "(" FormalParameterList ")" "{" FunctionBody "}" -- named
| function "(" FormalParameterList ")" "{" FunctionBody "}" -- anonymous
FormalParameterList = ListOf<FormalParameter, ",">
FormalParameter = identifier
/*
Note: The Directive Prologue is the longest sequence of ExpressionStatement
productions occurring as the initial SourceElement (see https://es5.github.io/#x14.1)
*/
FunctionBody = &(Directive*) SourceElement*
SourceElement = Declaration | Statement
// Broken out so es6 can override to include ConstDecl and LetDecl
Declaration = FunctionDeclaration
Directive = stringLiteral #sc
}
ES5Lax <: ES5 {
futureReservedWord := futureReservedWordLax
}

View file

@ -1,106 +0,0 @@
StronglyTypedWebLanguage {
Program = Statement*
Statement = ConstDec
| VarDec
| LiveDec
| ComputedDec
| TrackStmt
| ExprStmt
| Block
ConstDec = "const" identifier "=" Expr
VarDec = "var" identifier "=" Expr
LiveDec = "live" identifier "=" Expr
ComputedDec = "computed" identifier Block
TrackStmt = "track" ListOf<identifier, ","> LambdaParams? Block
ExprStmt = Expr // necessary?
Block = "{" Statement* "}"
LambdaParams = "|" ListOf<identifier, ","> "|"
ParExpr = "(" Expr ")"
Expr = AssignExpr
AssignExpr = CompareExpr ("=" CompareExpr)?
CompareExpr = AddExpr (CompareOp AddExpr)?
CompareOp = "==" | "!=" | "<=" | ">=" | "<" | ">"
AddExpr = MulExpr (("+"|"-") MulExpr)*
MulExpr = UnaryExpr (("*"|"/"|"%") UnaryExpr)*
UnaryExpr = "void" UnaryExpr -- voidExp
| "++" UnaryExpr -- preIncrement
| "--" UnaryExpr -- preDecrement
| "+" UnaryExpr -- unaryPlus
| "-" UnaryExpr -- unaryMinus
| "~" UnaryExpr -- bnot
| "!" UnaryExpr -- lnot
| PostfixExpr
| FunctionExpr
PostfixExpr = CallExpr "++" -- postIncrement
| CallExpr "--" -- postDecrement
| CallExpr
CallExpr = MemberExpr ("(" ListOf<Expr, ","> ")")?
MemberExpr = Literal ("." identifier)*
FunctionExpr = "fn" identifier "(" ListOf<identifier, ",">* ")" Block
Literal = numberLiteral
| stringLiteral
| booleanLiteral
| nullLiteral
| identifier
| ParExpr
| Block
// 123 or 12.34
numberLiteral = digit+ ("." digit+)?
stringLiteral = "\"" (~"\"" any)* "\""
| "'" (~"'" any)* "'"
| "`" (~"`" any)* "`"
identifier = ~reservedWord identifierStart (letter | digit | "$" | "_")*
identifierStart = letter | "$" | "_"
reservedWord = keyword | futureReservedWord | nullLiteral | booleanLiteral
nullLiteral = "null"
booleanLiteral = "true" | "false"
keyword = "const" | "var" | "live" | "computed" | "track" | "when" | "while"
| "if" | "else" | "for" | "unreachable" | "catch" | "async" | "await"
| "interface" | "struct" | "private" | "public" | "defer" | "fn"
| "break" | "void" | "return" | "import" | "export" | "switch"
| "continue"
futureReservedWord = "new" | "class" | "enum" | "extends" | "super"
| "implements" | "yield"
sourceCharacter = any
// Override Ohm's built-in definition of space.
space := whitespace | lineTerminator | comment
whitespace = "\t"
| "\x0B" -- verticalTab
| "\x0C" -- formFeed
| " "
| "\u00A0" -- noBreakSpace
| "\uFEFF" -- byteOrderMark
| unicodeSpaceSeparator
unicodeSpaceSeparator = "\u2000".."\u200B" | "\u3000"
lineTerminator = "\n" | "\r" | "\u2028" | "\u2029"
lineTerminatorSequence = "\n" | "\r" ~"\n" | "\u2028" | "\u2029" | "\r\n"
comment = multiLineComment | singleLineComment
multiLineComment = "/*" (~"*/" sourceCharacter)* "*/"
singleLineComment = "//" (~lineTerminator sourceCharacter)*
}