initial
This commit is contained in:
commit
ad3d1268a0
7 changed files with 350 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
68
README.md
Normal file
68
README.md
Normal 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
3
index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
'use string';
|
||||||
|
|
||||||
|
module.exports = require('./stwl')
|
26
package.json
Normal file
26
package.json
Normal 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
23
pnpm-lock.yaml
generated
Normal 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
123
stwl.js
Normal 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
106
stwl.ohm
Normal 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)*
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue