This commit is contained in:
Norman Köhring 2025-06-09 21:50:12 +02:00
commit ad3d1268a0
7 changed files with 350 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# STWL: Strongly Typed Web Language
Proposal: A strongly typed, programming language that compiles to JavaScript, with the following features:
* Error unions: `Error!string`
* Optional values: `?string`
* scoped blocks
* exhaustive pattern matching
* "unreachable" keyword
* built-in reactivity system (using signals?)
Syntax example (very early proposal, might change):
```ts
//imports work just as in ES6
import { func1, func2, func3 as funFunc } from './MyCoolModule'
import Cool from './MyCoolModule'
// although I might add something like this in the future:
import './MyCoolModule' as Cool // because it just reads so much nicer
// Declarations can be constant, variable or live (a.k.a. reactive):
// Constants can never change
const c = 299792468 // neither type nor value will ever change
c++ // Error! Constant values cannot change.
const car = struct{
tires: 4,
engine: true
}
car.tires = 5 // Error! Unlike in JS, struct values cannot be changed, either
// Variables can be changed, as long as their type stays the same:
var vehicle = struct{
tires: 2
engine: true
}
vehicle.engine = false // no problem
vehicle = struct{ // also fine
tires: 2
engine: false
}
vehicle = struct{ // Error! Type cannot be changed
color: 'red'
}
// Live values can be changed and their changes are tracked
live speed = 50
track speed {
console.log(`Driving ${speed} km/h now`)
}
speed++ // logs speed automatically
speed = 'fast' // gives an error, because the type cannot change
// Computed values are basically getters (similar to Vuejs' computed)
computed apparentSpeed {
// pattern matching is another useful language feature that is explained in
// further detail later
return match speed {
10 -> 'walking speed'
_ < 50 -> 'inside town, maybe'
_ -> 'outside town, hopefully'
}
}
```

3
index.js Normal file
View file

@ -0,0 +1,3 @@
'use string';
module.exports = require('./stwl')

26
package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "ohm-grammar-stwl",
"version": "0.0.1",
"description": "Strongly Typed Web Language",
"license": "MIT",
"keywords": [
"ohm",
"ohm-grammar",
"javascript",
"typed",
"peg",
"stwl"
],
"author": "koehr <n@koehr.ing>",
"homepage": "https://git.koehr.ing/n/ohm-grammar-stwl",
"bugs": {
"url": "https://git.koehr.ing/n/ohm-grammar-stwl/issues"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"packageManager": "pnpm@10.5.2",
"devDependencies": {
"ohm-js": "^17.1.0"
}
}

23
pnpm-lock.yaml generated Normal file
View file

@ -0,0 +1,23 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
ohm-js:
specifier: ^17.1.0
version: 17.1.0
packages:
ohm-js@17.1.0:
resolution: {integrity: sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==}
engines: {node: '>=0.12.1'}
snapshots:
ohm-js@17.1.0: {}

123
stwl.js Normal file
View file

@ -0,0 +1,123 @@
/* 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,
};

106
stwl.ohm Normal file
View file

@ -0,0 +1,106 @@
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)*
}