From ad3d1268a0f6e2d40f827a59f8e6520e5d28a94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20K=C3=B6hring?= Date: Mon, 9 Jun 2025 21:50:12 +0200 Subject: [PATCH] initial --- .gitignore | 1 + README.md | 68 +++++++++++++++++++++++++++ index.js | 3 ++ package.json | 26 +++++++++++ pnpm-lock.yaml | 23 +++++++++ stwl.js | 123 +++++++++++++++++++++++++++++++++++++++++++++++++ stwl.ohm | 106 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 stwl.js create mode 100644 stwl.ohm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..1edda45 --- /dev/null +++ b/README.md @@ -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' + } + } + +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..1fb1510 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use string'; + +module.exports = require('./stwl') diff --git a/package.json b/package.json new file mode 100644 index 0000000..83b1d54 --- /dev/null +++ b/package.json @@ -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 ", + "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" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..fab1001 --- /dev/null +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/stwl.js b/stwl.js new file mode 100644 index 0000000..62758a6 --- /dev/null +++ b/stwl.js @@ -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, +}; + diff --git a/stwl.ohm b/stwl.ohm new file mode 100644 index 0000000..955612c --- /dev/null +++ b/stwl.ohm @@ -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 LambdaParams? Block + ExprStmt = Expr // necessary? + + Block = "{" Statement* "}" + LambdaParams = "|" ListOf "|" + + 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 ")")? + MemberExpr = Literal ("." identifier)* + + FunctionExpr = "fn" identifier "(" ListOf* ")" 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)* +}