From 9f690b8039b9de0db654cdccf6c800a0bfab37c7 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 06:20:34 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20CodeRabbit=20Chat:=20Implement?= =?UTF-8?q?=20Bootstrapped=20BynixScript=20v0.3.0=20Compiler=20and=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmignore | 3 +- README.md | 40 +++- js/browser.js | 405 +++---------------------------------- package-lock.json | 2 +- package.json | 2 +- scripts/browser-entry.cjs | 27 +++ scripts/build.cjs | 33 ++- scripts/compile.cjs | 14 +- src/parser/assignDecl.bs | 9 - src/parser/asyncDecl.bs | 9 - src/parser/checkDecl.bs | 14 -- src/parser/commentDecl.bs | 8 - src/parser/compiler.bs | 173 ++++++++++++++++ src/parser/condDecl.bs | 11 - src/parser/convDecl.bs | 16 -- src/parser/domDecl.bs | 17 -- src/parser/excepDecl.bs | 19 -- src/parser/expDecl.bs | 6 - src/parser/flowDecl.bs | 9 - src/parser/forDecl.bs | 8 - src/parser/forEachDecl.bs | 7 - src/parser/funcDecl.bs | 6 - src/parser/interactDecl.bs | 8 - src/parser/lexer.bs | 131 ++++++++++++ src/parser/logDecl.bs | 12 -- src/parser/mathDecl.bs | 10 - src/parser/oopDecl.bs | 13 -- src/parser/otherDecl.bs | 7 - src/parser/parsingDecl.bs | 22 -- src/parser/reassignDecl.bs | 6 - src/parser/syntax.bs | 297 +++++++++++++++++++++++++++ src/parser/utilityDecl.bs | 21 -- src/translate.bs | 13 +- tests/cli.test.cjs | 2 +- tests/compiler.test.cjs | 135 +++++++++++++ 35 files changed, 872 insertions(+), 643 deletions(-) create mode 100644 scripts/browser-entry.cjs delete mode 100644 src/parser/assignDecl.bs delete mode 100644 src/parser/asyncDecl.bs delete mode 100644 src/parser/checkDecl.bs delete mode 100644 src/parser/commentDecl.bs create mode 100644 src/parser/compiler.bs delete mode 100644 src/parser/condDecl.bs delete mode 100644 src/parser/convDecl.bs delete mode 100644 src/parser/domDecl.bs delete mode 100644 src/parser/excepDecl.bs delete mode 100644 src/parser/expDecl.bs delete mode 100644 src/parser/flowDecl.bs delete mode 100644 src/parser/forDecl.bs delete mode 100644 src/parser/forEachDecl.bs delete mode 100644 src/parser/funcDecl.bs delete mode 100644 src/parser/interactDecl.bs create mode 100644 src/parser/lexer.bs delete mode 100644 src/parser/logDecl.bs delete mode 100644 src/parser/mathDecl.bs delete mode 100644 src/parser/oopDecl.bs delete mode 100644 src/parser/otherDecl.bs delete mode 100644 src/parser/parsingDecl.bs delete mode 100644 src/parser/reassignDecl.bs create mode 100644 src/parser/syntax.bs delete mode 100644 src/parser/utilityDecl.bs create mode 100644 tests/compiler.test.cjs diff --git a/.npmignore b/.npmignore index 89a95c1..0d0dbd5 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ /docs/ /example/ /js/* +!/js/browser.js /.git/ /.github/ bsc.json @@ -11,4 +12,4 @@ CONTRIBUTING.md *.tgz esbuild.config.js *.map -/.trash/ \ No newline at end of file +/.trash/ diff --git a/README.md b/README.md index 82dc81a..c2b248b 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,42 @@ npm ci npm run build npm test ``` -The build bootstraps the `.bs` CLI sources with the BynixScript compiler from the -`bynixscript` development dependency, then bundles the compiled JavaScript with -esbuild into `dist/index.min.cjs`. `npm test` also checks that every CLI source -compiles through the same compiler path. +The build pins `bynixscript@0.2.1-next` for **bootstrap only**. It compiles +`src/parser/lexer.bs`, `src/parser/syntax.bs`, and `src/parser/compiler.bs` +with that version, checks that the resulting compiler can compile its own sources, +then compiles the CLI sources with the generated compiler. esbuild packages the +CLI as `dist/index.min.cjs` and the same compiler for the browser as +`js/browser.js`. Browser and CLI do not use the old regex translator at runtime. + +### Native compiler subset (v0.3.0 development) + +Tokens include identifiers, decimal numbers, quoted and template strings, +operators, punctuation, newlines, and EOF; `#`, `//`, and `/* ... */` comments +are ignored. Tokens and syntax-tree nodes carry one-based line and column +positions; malformed tokens, expressions, and blocks throw `SyntaxError` with +the offending position. Blocks are terminated by `end`, not indentation; +`elif` and `else` belong to the nearest open `if`, and `recovery`/`final` to +the nearest `handle` block. + +Supported statements: `func`, `if`/`elif`/`else`, `for ... of`, `while`, +`handle`/`recovery`/`final`, declarations, assignment, `return`, `throw`, +`break`, `continue`, and expression statements. Expression nodes include +literals, unary and binary operators, calls, members, indexes, arrays, objects, +grouping, and conditional expressions. Operator precedence from lowest to +highest: assignment; `||`/`??`; `&&`; equality; comparisons; `+`/`-`; +`*`/`/`/`%`; `**`; unary; call/member/index. Assignments and exponentiation +associate right-to-left. Strings remain untouched by keyword translation. + +Bootstrap sources use v0.2.1-compatible `func`/`if`/`for ... of`/`end` syntax; +the build protects quoted literals from its regex passes and uses JavaScript +`while (...) { ... }` blocks where v0.2.1 lacks a `while` form. No regex pass +is used on user programs. The previous documented `match`/`case`, classes, +`delay`/`repeat`, listener/iteration callback blocks, and DOM-specific +`is_*` properties are not yet implemented and produce syntax errors instead +of falling back to regex substitution. Regular-expression literals, arrow +functions, multiline template interpolation of BynixScript syntax, and +JavaScript-style function declarations are also outside this subset. The +package remains at `0.2.2-next` until these syntax gaps are addressed. ### Using CDN ```html diff --git a/js/browser.js b/js/browser.js index 88af974..40cad01 100644 --- a/js/browser.js +++ b/js/browser.js @@ -1,382 +1,23 @@ -let parsingResults; -let allowJs; -const style = document.createElement("style"); -style.textContent = ` - bynix { - display: none; - } - `; -style.classList.add("bynix-style"); -document.head.appendChild(style); -const milown = document.createElement("script"); -const ajax = document.createElement("script"); -const fontawesome = document.createElement("link"); -const brython = document.createElement("script"); -milown.src = ''; -ajax.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/4.0.0-beta.2/jquery.min.js"'; -fontawesome.rel = "stylesheet"; -fontawesome.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'; -brython.type = "type/javascript"; -brython.src = 'https://cdn.jsdelivr.net/npm/brython@3.9.6/brython.min.js'; -document.head.appendChild(ajax); -document.head.appendChild(fontawesome); -document.head.appendChild(milown); -document.head.appendChild(brython); -document.addEventListener("DOMContentLoaded", function() { - const bynixTags = document.querySelectorAll("bynix"); - bynixTags.forEach(tag => { - var code = ''; - const src = tag.getAttribute('src'); - const allowJsTag = tag.getAttribute('allowJs'); - - if (allowJsTag === null) { - allowJs = "false"; - } else { - allowJs = allowJsTag; - } - - // Jika src ada, lakukan fetch - if (src) { - const end = src.substring(src.lastIndexOf('.') + 1); - - if (src.endsWith(".bs") || src.endsWith(".bynixscript") || src.endsWith(".mbs")) { - fetch(src) - .then(response => response.text()) - .then(srcCode => { - code = parseCode(srcCode); - parsingResults = parsing(code); - code = addSemicolons(code); - executeCode(code); - tag.remove(); - }) - .catch(error => console.error('Error fetching BynixScript code:', error)); - } else { - console.error("Invalid extension '." + end + "'"); - } - } else { - // Jika src tidak ada, ambil kode dari textContent - code = parseCode(tag.textContent); - parsingResults = parsing(code); - code = addSemicolons(code); - executeCode(code); - tag.remove(); - } - }); -}); - -function executeCode(code) { - if (parsingResults === "false") { - console.error(`Please check your code and try again.`); - } else { - // function - code = code.replace(/func\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\):/g, (match, p1, p2) => `function ${p1}(${p2}) {`); - - // if, elif, else - code = code.replace(/elif\s+(.+?):/g, (match, p1) => `} else if (${p1}) {`); - code = code.replace(/if\s+(.+?):/g, (match, p1) => `if (${p1}) {`); - code = code.replace(/else:/g, "} else {"); - - // forEach - code = code.replace(/forEach\((.+?)\)\:/g, (match, p1) => `forEach(${p1} => {`); - code = code.replace(/forEach\((.+?)\s=>\s(.+?)\)/g, (match, p1, p2) => `forEach(${p1} => ${p2})`); - - // var, const - code = code.replace(/var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `var ${p1} = ${p2};`); - code = code.replace(/const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `const ${p1} = ${p2};`); - - // print, print.err, print.info, print.debug, print.trace, print.warn, print.assert - code = code.replace(/print(\.[a-zA-Z]+)?\((.+?)\)/g, (match, p1, p2) => { - const type = p1 || ''; - - if (type.includes(".info") || type.includes(".debug") || type.includes(".trace") || type.includes(".warn") || type.includes(".assert")) { - return `console${p1}(${p2})` - } else if (type.includes(".err")) { - return `console.error(${p2})` - } else if (match.includes("/")) { - return match; - } else { - return `console.log(${p2})` - } - }); - - // touch, ask, confirm - code = code.replace(/touch\((.+?)\)/g, (match, p1) => `alert(${p1});`); - code = code.replace(/ask\((.+?)\)/g, (match, p1) => `prompt(${p1});`); - code = code.replace(/confirm\((.+?)\)/g, (match, p1) => `confirm(${p1});`); - - // rand, roundDown, roundUp, maxOf, minOf - code = code.replace(/rand\((.+?)\)/g, (match, p1) => `Math.random(${p1})`); - code = code.replace(/roundDown\((.+?)\)/g, (match, p1) => `Math.floor(${p1})`); - code = code.replace(/roundUp\((.+?)\)/g, (match, p1) => `Math.ceil(${p1})`); - code = code.replace(/maxOf\((.+?)\)/g, (match, p1) => `Math.min(${p1})`); - code = code.replace(/minOf\((.+?)\)/g, (match, p1) => `Math.max(${p1})`); - - // getElement, getAllElement, createElement, addClass, addElement, listener - code = code.replace(/getElement\((.+?)\)/g, (match, p1) => `document.querySelector(${p1});`); - code = code.replace(/getAllElement\((.+?)\)/g, (match, p1) => `document.querySelectorAll(${p1});`); - code = code.replace(/createElement\((.+?)\)/g, (match, p1) => `document.createElement(${p1});`); - code = code.replace(/\.addClass\((.+?)\)/g, (match, p1) => `.classList.add(${p1})`); - code = code.replace(/([^\s].+)\.addElement\((.+?)\)/g, (match, p1, p2) => `document.${p1}.appendChild(${p2});`); - code = code.replace(/(.+?)\.listener\((.+?)\)\:/g, (match, p1, p2) => `${p1}.addEventListener(${p2}, () => {`) - code = code.replace(/listener\((.+?)\)\:/g, (match, p1) => `document.addEventListener(${p1}, function() {`) - - // change - code = code.replace(/(.+?)\.change\((.+?), \((.+?)\)\)\:/g, (match, p1, p2, p3) => `${p1}.replace(${p2}, (${p3}) => {`) - code = code.replace(/(.+?)\.change\((.+)\)/g, (match, p1, p2) => `${p1}.replace(${p2})`); - - // text - code = code.replace(/(.+?)\.text/g, (match, p1) => `${p1}.textContent`) - code = code.replace(/(.+?)\.text = (.+?)/g, (match, p1, p2) => `${p1}.textContent = ${p2}`); - code = code.replace(/(? `//${p1}`); - code = code.replace(/\*\*(.+?)/g, (match, p1) => `/*${p1}`); - code = code.replace(/(.+?)\*\*/g, (match, p1) => `${p1}*/`); - - // condition property - code = code.replace(/is_includes\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1) => `includes(${p1})`); - code = code.replace(/is_start\s===\s([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)/g, (match, p1) => `startsWith(${p1})`); - code = code.replace(/is_end\s===\s([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)/g, (match, p1) => `endsWith(${p1})`); - code = code.replace(/is_matched\s===\s"(.+?)"/g, (match, p1) => `match(${p1})`); - code = code.replace(/is_matched\s===\s'(.+?)'/g, (match, p1) => `match(${p1})`); - code = code.replace(/is_value\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1) => `value === ${p1}`); - code = code.replace(/is_design.(.+?)\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1, p2) => `style.${p1} === ${p2}`); - code = code.replace(/(.+?)\.design\.(.+?)\s=\s(.+?)/g, (match, p1, p2, p3) => `${p1}.style.${p2} = ${p3}`); - code = code.replace(/(.+?)\.design\s=\s(.+?)/g, (match, p1, p2) => `${p1}.style = ${p2}`); - code = code.replace(/is_action\s([=!><]+)\s(["'])checked(["'])/g, (match) => `checked`); - code = code.replace(/is_action\s([=!><]+)\s(["'])selected(["'])/g, (match) => `selected`); - code = code.replace(/is_action\s([=!><]+)\s(["'])open(["'])/g, (match) => `open`); - code = code.replace(/is_have\s([=!><]+)\s(["'])required(["'])/g, (match) => `required`); - code = code.replace(/is_have\s([=!><]+)\s(["'])readOnly(["'])/g, (match) => `readOnly`); - code = code.replace(/is_have\s([=!><]+)\s(["'])autofocus(["'])/g, (match) => `autofocus`); - code = code.replace(/is_have\s([=!><]+)\s(["'])disabled(["'])/g, (match) => `disabled`); - code = code.replace(/is_have\s([=!><]+)\s(["'])multiple(["'])/g, (match) => `multiple`); - code = code.replace(/is_have\s([=!><]+)\s(["'])hidden(["'])/g, (match) => `hidden`); - code = code.replace(/delay\:\n\s+(.+?)/g, (match, p1) => `setTimeout(function() {\n ${p1}`); - - // loop - code = code.replace(/repeat\:\n\s+(.+?)/g, (match, p1) => `setInterval(function() {\n ${p1}`); - code = code.replace(/for (.+?)=(.+?) to (.+?)\:/g, (match, p1, p2, p3) => `for (let ${p1} = ${p2}; ${p1} < ${p3}; ${p1}++) {`); - code = code.replace(/for (.+?) in (.+?)\:/g, (match, p1, p2) => `for (let ${p1} in ${p2}) {`); - code = code.replace(/for (.+?) of (.+?)\:/g, (match, p1, p2) => `for (let ${p1} of ${p2}) {`); - code = code.replace(/(? `switch ((${p1})) {`) - code = code.replace("switch ((", "switch (") - code = code.replace(")) {", ") {") - code = code.replace(/case (.+?):\n(.+?)/g, (match, p1, p2) => `case ${p1}: \n${p2}`) - code = code.replace(/([^\s].+?)\.image\s*=\s*(['"`][^'"`]*['"``])/g, (match, p1, p2) => `image(${p1}, ${p2})`); - code = code.replace(/process\.argv\[(\d+)\]/g, (match, p1) => { - var a = Number(p1) + 0; - return `process.argv[${a}]`; - }); - code = code.replace(/int\((.+?)\)/g, (match, p1) => `parseInt(${p1})`) - code = code.replace(/num\((.+?)\)/g, (match, p1) => `Number(${p1})`) - code = code.replace(/str\((.+?)\)/g, (match, p1) => `String(${p1})`) - code = code.replace(/float\((.+?)\)/g, (match, p1) => `parseFloat(${p1})`) - code = code.replace(/json\((.+?)\)/g, (match, p1) => `JSON.parse(${p1})`); - code = code.replace(/(.+?)\.map\((.+?)\)\:/g, (match, p1, p2) => `${p1}.map(function(${p2}) {`) - code = code.replace(/\.map\((.+?)\)/g, (match, p1) => `.map(${p1})`); - code = code.replace(/(.+?)\.filter\((.+?)\)\:/g, (match, p1, p2) => `${p1}.filter(function(${p2}) {`); - code = code.replace(/\.filter\((.+?)\)/g, (match, p1) => `.filter(${p1})`); - code = code.replace(/(.+?)\.reduce\((.+?)\)\:/g, (match, p1, p2) => `${p1}.reduce(function(${p2}) {`) - code = code.replace(/\.reduce\((.+?)\)/g, (match, p1) => `.reduce(${p1})`); - code = code.replace(/(? `typeof ${p1}`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_type\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1, p2) => `typeof ${p1} === ${p2}`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])type(["'])/g, (match, p1) => `${p1} instanceof TypeError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])reference(["'])/g, (match, p1) => `${p1} instanceof ReferenceError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])syntax(["'])/g, (match, p1) => `${p1} instanceof SyntaxError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])range(["'])/g, (match, p1) => `${p1} instanceof RangeError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])uri(["'])/g, (match, p1) => `${p1} instanceof URIError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])eval(["'])/g, (match, p1) => `${p1} instanceof EvalError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])(.+?)(["'])/g, (match, p1, p2, p3) => `${p1} instanceof ${p3}`) - code = code.replace(/constructor\((.+?)\)\:/g, (match, p1) => `constructor(${p1}) {`); - code = code.replace(/(.+?)\(\)\:/g, (match, p1) => `${p1}() {`); - code = code.replace(/class\s(.+?)\:/g, (match, p1) => `class ${p1} {`); - code = code.replace(/class\s(.+?)\sextends\s(.+?)\:/g, (match, p1, p2) => `class ${p1} extends ${p2} {`); - code = code.replace(/static\((.+?)\)\ {/g, (match, p1) => `static(${p1}) {`); - code = code.replace(/super\((.+?)\)\:/g, (match, p1) => `super(${p1})`) - code = code.replace(/throw (.+?)/g, (match, p1) => `throw ${p1}`) - code = code.replace(/new (.+?)/g, (match, p1) => `new ${p1}`) - code = code.replace(/end\:(.+?)/g, (match, p1) => `}, ${p1});`); - code = code.replace(/handle\:/g, 'try {') - code = code.replace(/recovery\s\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - code = code.replace(/final\:/g, "} finally {") - code = code.replace(/handle\:/g, 'try {') - code = code.replace(/recovery\s*\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - code = code.replace(/final\:/g, "} finally {") - code = code.replace(/Err\((.+?)\)/g, (match, p1) => `Error(${p1})`) - code = code.replace(/URIErr\((.+?)\)/g, (match, p1) => `URIError(${p1})`) - code = code.replace(/EvalErr\((.+?)\)/g, (match, p1) => `EvalError(${p1})`) - code = code.replace(/TypeErr\((.+?)\)/g, (match, p1) => `TypeError(${p1})`) - code = code.replace(/RangeErr\((.+?)\)/g, (match, p1) => `RangeError(${p1})`) - code = code.replace(/SyntaxErr\((.+?)\)/g, (match, p1) => `Error(${p1})`) - code = code.replace(/InternalErr\((.+?)\)/g, (match, p1) => `InternalError(${p1})`) - code = code.replace(/ReferenceErr\((.+?)\)/g, (match, p1) => `ReferenceError(${p1})`) - - const script = document.createElement("script"); - script.textContent = code; - document.body.appendChild(script); - } -} - -function image(element, imageClass) { - if (!element) { - console.error(`Element tidak ditemukan.`); - return; - } - - const icon = document.createElement("i"); - - // Pisahkan class yang diberikan berdasarkan spasi - const classes = imageClass.split(' '); - classes.forEach(cls => icon.classList.add(cls)); - - element.appendChild(icon); -} - -function parsing(variable) { - const patterns = [ - /let\s*(.+?)=(.+?)/g, - /function\s*(.+?)\((.+?)\)/g, - /if\s*\((.+?)\)\s*{/g, - /else if\s*\((.+?)\)\s*{/g, - /else\s*{\s*/g, - /console(.+?)\((.+?)\)/g, - /alert\((.+?)\)/g, - /prompt\((.+?)\)/g, - /Math\.random\((.+?)\)/g, - /Math\.floor\((.+?)\)/g, - /Math\.ceil\((.+?)\)/g, - /Math\.min\((.+?)\)/g, - /Math\.max\((.+?)\)/g, - /document\.createElement\((.+?)\)/g, - /document\.(.+?)\.appendChild\((.+?)\)/g, - /(.+?).replace\((.+?)\)/g, - /\/\//g, - /(.+?)\.textContent\s*=\s*(.+?)/g, - /\/\*(.+?)\*\//g, - /document\.addEventListener\((.+?), function\(\) \{/g, - /forEach\((.+?) \=\> \{/g, - /(.+?)\.includes\((.+?)\)/g, - /(.+?)\.match\((.+?)\)/g, - /(.+?)\.value ===/g, - /(.+?)\.value ==/g, - /(.+?)\.value =/g, - /(.+?)\.value !===/g, - /(.+?)\.value !==/g, - /(.+?)\.value !=/g, - /(.+?)\.value >/g, - /(.+?)\.value /g, - /(.+?)\.value <==/g, - /(.+?)\.style ===/g, - /(.+?)\.style ==/g, - /(.+?)\.style =/g, - /(.+?)\.style !===/g, - /(.+?)\.style !==/g, - /(.+?)\.style !=/g, - /(.+?)\.style >/g, - /(.+?)\.style /g, - /(.+?)\.style <==/g, - /(.+?)\.style\.(.+?) =/g, - /(.+?)\.style =/g, - /(.+?)\.checked/g, - /(.+?)\.required/g, - /setTimeout\(function\(\) \{/g, - /setInterval\(function\(\) \{/g, - /for \((.+?)\) \{/g, - /classList\.add\((.+?)\)/g, - /switch\s*\((.+?)\)/g, - /image\((.+?), (.+?)\)/g, - /class\s*(.+?)\s*\{/g, - /class\s*(.+?)\s*extends\s*(.+?)\s*\{/g, - /constructor(.+?)\s*\{/g, - /(.+?)\(\)\s*\{/g, - /static(.+?)\s*\{/g, - /typeof\s*(.+?)\s*(.+?)\s*(.+?)/g, - /\.map\(function\((.+?)\)\s*\{/g, - /\.filter\(function\((.+?)\)\s*\{/g, - /\.reduce\(function\((.+?)\)\s*\{/g, - /try\s*{/g, - /}\s*catch\s*(.+?)\s*{/g, - /}\s*finally\s*{/g, - ]; - - if (allowJs === "false") { - const matches = patterns.map(pattern => { - const match = variable.match(pattern); - return match ? match[0] : null; - }).filter(Boolean); - - if (matches.length > 0) { - parsingMsg = "Invalid function '" + matches.join("', '") + "'"; - const error = new Error(parsingMsg); - console.error(error); - return "false"; - } else { - parsingMsg = ""; - return "true"; - } - } -} - -function parseCode(code) { - const lines = code.split('\n'); - const output = []; - const stack = []; - - const regex = { - listener: /(.+?)\.listener/, - classFunc: /(.+?)\(\)\:/, - change: /(.+?)\.change\((.+?), \((.+?)\)\)\:/, - map: /(.+?)\.map\((.+?)\)\:/, - filter: /(.+?)\.filter\((.+?)\)\:/, - forEach: /forEach\((.+?)\)\:/, - func: /func\s*(.+?)\((.+?)\)/, - blockStart: /^(if|elif|else|class|constructor|static|final|recovery|func)/, - }; - - for (let line of lines) { - const trimmedLine = line.trim(); - - if (regex.blockStart.test(trimmedLine)) { - stack.push('block'); - output.push(line); - } else if (regex.listener.test(trimmedLine) || regex.change.test(trimmedLine) || - regex.map.test(trimmedLine) || regex.filter.test(trimmedLine) || regex.forEach.test(trimmedLine)) { - stack.push('listener'); - output.push(line); - } else if (regex.func.test(trimmedLine)) { - stack.push('func'); - output.push(line); - } else if (trimmedLine === 'end') { - const indent = line.match(/^\s*/)[0]; - const lastBlock = stack.pop(); - output.push(lastBlock === 'listener' ? `${indent}});` : `${indent}}`); - } else { - output.push(line); - } - } - - return output.join('\n'); -} - -function addSemicolons(code) { - const lines = code.split('\n'); - const output = []; - - const regex = { - needsSemicolon: /^(var|const|return|throw|break|continue|.*\=.*|[^;{}]+\))$/, - alreadyTerminated: /[:;{}]\s*$/ - }; - - for (let line of lines) { - const trimmedLine = line.trim(); - - if (regex.needsSemicolon.test(trimmedLine) && !regex.alreadyTerminated.test(trimmedLine)) { - output.push(line + ';'); - } else { - output.push(line); - } - } - - return output.join('\n'); -} +(()=>{var U=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports);var M=U((T,_)=>{function I(e){let n=new RegExp("^[a-zA-Z_$]$"),i=new RegExp("^[a-zA-Z0-9_$]$"),r=new RegExp("^[0-9]$"),u=new RegExp("^[0-9]+(\\.[0-9]+)?$"),t=[];for(var l=0,g=1,s=1,$=0;l=e.length)break;l+=2,s+=2}else if(e[l]===E){l+=1,s+=1,S=!0;break}else if(e[l]===` +`){if(E!=="`")throw new SyntaxError(`Unterminated string at ${m}:${x}`);l+=1,g+=1,s=1}else l+=1,s+=1;if(!S)throw new SyntaxError(`Unterminated string at ${m}:${x}`);t.push({kind:"string",value:e.slice(v,l),line:m,column:x})}else if(n.test(h)){for(;l>>"].includes(E)?o=E:["==","!=","<=",">=","&&","||","??","+=","-=","*=","/=","++","--","**","=>","?."].includes(A)&&(o=A),!"()[]{}.,:;?+-*/%!=<>|&".includes(h))throw new SyntaxError(`Unexpected character ${h} at ${m}:${x}`);if(l+=o.length,s+=o.length,o==="("||o==="[")$+=1;else if((o===")"||o==="]")&&($-=1,$<0))throw new SyntaxError(`Unexpected ${o} at ${m}:${x}`);t.push({kind:"symbol",value:o,line:m,column:x})}}return t.push({kind:"eof",value:"",line:g,column:s}),t}_.exports={lex:I}});var R=U((V,O)=>{var{lex:P}=M();function f(e){return e.tokens[e.index]}function w(e,n){let i=f(e);throw new SyntaxError(`${n} at ${i.line}:${i.column}`)}function y(e,n){return f(e).value===n?(e.index+=1,!0):!1}function a(e,n){y(e,n)||w(e,`Expected ${n}`)}function q(e){let n=f(e);return n.kind!=="identifier"&&w(e,"Expected identifier"),e.index+=1,n.value}function C(e){for(;f(e).kind==="newline"||f(e).value===";";)e.index+=1}function W(e){let n=[["=","+=","-=","*=","/="],["||","??"],["&&"],["==","!=","===","!=="],["<",">","<=",">=","in","instanceof"],["+","-"],["*","/","%"],["**"]];for(var i=0;i{var{parse:B}=R();function j(e,n){throw new SyntaxError(`${n} at ${e.line}:${e.column}`)}function c(e){if(e.type==="literal")return["class","switch","async","await","import","export","yield","function"].includes(e.value)&&j(e,`Unsupported syntax ${e.value}`),e.value==="Err"?"Error":e.value;if(e.type==="member"){var n=e.property;if(n==="is_end")n="endsWith";else if(n==="is_start")n="startsWith";else if(n==="is_includes")n="includes";else if(n==="is_matched")n="match";else if(n==="text")n="textContent";else if(n==="design")n="style";else{if(n==="addClass")return`${c(e.object)}.classList.add`;n==="addElement"?n="appendChild":n==="change"?n="replace":n.slice(0,3)==="is_"&&j(e,`Unsupported property ${n}`)}return`${c(e.object)}${e.optional?"?.":"."}${n}`}else{if(e.type==="index")return`${c(e.object)}[${c(e.property)}]`;if(e.type==="call"){var i=c(e.callee);e.callee.type==="literal"?e.callee.value==="print"?i="console.log":e.callee.value==="int"?i="parseInt":e.callee.value==="num"?i="Number":e.callee.value==="str"?i="String":e.callee.value==="float"?i="parseFloat":e.callee.value==="json"?i="JSON.parse":e.callee.value==="touch"?i="alert":e.callee.value==="ask"?i="prompt":e.callee.value==="rand"?i="Math.random":e.callee.value==="roundDown"?i="Math.floor":e.callee.value==="roundUp"?i="Math.ceil":e.callee.value==="maxOf"?i="Math.max":e.callee.value==="minOf"?i="Math.min":e.callee.value==="getElement"?i="document.querySelector":e.callee.value==="getAllElement"?i="document.querySelectorAll":e.callee.value==="createElement"&&(i="document.createElement"):e.callee.type==="member"&&e.callee.object.value==="print"&&["err","info","warn","debug","trace","assert"].includes(e.callee.property)&&(i=`console.${e.callee.property==="err"?"error":e.callee.property}`);let r=[];for(let u of e.args)r.push(c(u));return`${i}(${r.join(", ")})`}else{if(e.type==="binary")return`(${c(e.left)} ${e.operator} ${c(e.right)})`;if(e.type==="unary")return`${e.operator}${["new","typeof","void","delete"].includes(e.operator)?" ":""}${c(e.argument)}`;if(e.type==="postfix")return`${c(e.argument)}${e.operator}`;if(e.type==="conditional")return`(${c(e.condition)} ? ${c(e.yes)} : ${c(e.no)})`;if(e.type==="group")return`(${c(e.value)})`;if(e.type==="array"){let r=[];for(let u of e.items)r.push(c(u));return`[${r.join(", ")}]`}else if(e.type==="object"){let r=[];for(let u of e.entries)u.value===null?r.push(u.key):r.push(`${u.key}: ${c(u.value)}`);return`{ ${r.join(", ")} }`}}}j(e,`Unsupported expression ${e.type}`)}function k(e){let n=[];for(let i of e)n.push(z(i));return n.join(` +`)}function z(e){if(e.type==="function")return`function ${e.name}(${e.parameters.join(", ")}) { +${k(e.body)} +}`;if(e.type==="if"){let i=[];for(let r of e.branches)i.push(`${i.length===0?"if":"else if"} (${c(r.condition)}) { +${k(r.body)} +}`);return e.otherwise!==null&&i.push(`else { +${k(e.otherwise)} +}`),i.join(" ")}else{if(e.type==="for")return`for (${e.declaration===null?"":e.declaration+" "}${c(e.binding)} of ${c(e.iterable)}) { +${k(e.body)} +}`;if(e.type==="while")return`while (${c(e.condition)}) { +${k(e.body)} +}`;if(e.type==="try"){e.recovery===null&&e.final===null&&j(e,"A recovery or final block is required");var n=`try { +${k(e.body)} +}`;return e.recovery!==null&&(n+=` catch (${e.recovery.name}) { +${k(e.recovery.body)} +}`),e.final!==null&&(n+=` finally { +${k(e.final)} +}`),n}else{if(e.type==="declaration")return e.value===null&&e.kind==="const"&&j(e,"A constant requires an initializer"),`${e.kind} ${c(e.name)}${e.value===null?"":" = "+c(e.value)};`;if(e.type==="return"||e.type==="throw")return`${e.type}${e.value===null?"":" "+c(e.value)};`;if(e.type==="break"||e.type==="continue")return`${e.type};`;if(e.type==="expression")return`${c(e.value)};`}}j(e,`Unsupported statement ${e.type}`)}function G(e){return k(B(e).body)}L.exports={compileSource:G,emitExpression:c,emitStatement:z}});var K=U(()=>{var{compileSource:H}=N();document.addEventListener("DOMContentLoaded",()=>{for(let e of document.querySelectorAll("bynix")){let n=e.getAttribute("src");n?fetch(n).then(i=>{if(!i.ok)throw new Error(`Unable to fetch ${n}: ${i.status}`);return i.text()}).then(i=>D(e,i)).catch(i=>console.error(i)):D(e,e.textContent)}});function D(e,n){try{(0,eval)(H(n)),e.remove()}catch(i){console.error(i)}}});K();})(); diff --git a/package-lock.json b/package-lock.json index 5b2d4cb..2fb0040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "bynix": "dist/index.min.cjs" }, "devDependencies": { - "bynixscript": "^0.2.1-next", + "bynixscript": "0.2.1-next", "esbuild": "^0.25.10" } }, diff --git a/package.json b/package.json index 6ca3865..3084e34 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "Indonesian" ], "devDependencies": { - "bynixscript": "^0.2.1-next", + "bynixscript": "0.2.1-next", "esbuild": "^0.25.10" }, "repository": "https://github.com/UngGasStudio/BynixScript/" diff --git a/scripts/browser-entry.cjs b/scripts/browser-entry.cjs new file mode 100644 index 0000000..8c695f2 --- /dev/null +++ b/scripts/browser-entry.cjs @@ -0,0 +1,27 @@ +const { compileSource } = require('bynix-native') + +document.addEventListener('DOMContentLoaded', () => { + for (const tag of document.querySelectorAll('bynix')) { + const src = tag.getAttribute('src') + if (src) { + fetch(src) + .then(response => { + if (!response.ok) throw new Error(`Unable to fetch ${src}: ${response.status}`) + return response.text() + }) + .then(source => execute(tag, source)) + .catch(error => console.error(error)) + } else { + execute(tag, tag.textContent) + } + } +}) + +function execute(tag, source) { + try { + ;(0, eval)(compileSource(source)) + tag.remove() + } catch (error) { + console.error(error) + } +} diff --git a/scripts/build.cjs b/scripts/build.cjs index a411f77..2ccdd46 100644 --- a/scripts/build.cjs +++ b/scripts/build.cjs @@ -1,7 +1,8 @@ const fs = require('node:fs') const path = require('node:path') const { build } = require('esbuild') -const { compileSource } = require('./compile.cjs') +const { compileSource: bootstrapSource } = require('./compile.cjs') +const { Script } = require('node:vm') const root = path.resolve(__dirname, '..') const temporary = fs.mkdtempSync(path.join(root, '.bynix-build-')) @@ -10,10 +11,28 @@ const output = path.join(root, 'dist', 'index.min.cjs') async function main() { try { + const parserSource = path.join(source, 'parser') + const parserOutput = path.join(temporary, 'parser') + fs.mkdirSync(parserOutput) + for (const name of ['lexer', 'syntax', 'compiler']) { + const code = bootstrapSource(fs.readFileSync(path.join(parserSource, `${name}.bs`), 'utf8')) + new Script(code, { filename: `${name}.js` }) + fs.writeFileSync(path.join(parserOutput, `${name}.js`), code) + } + const nativeCompiler = require(path.join(parserOutput, 'compiler.js')).compileSource + for (const name of ['lexer', 'syntax', 'compiler']) { + new Script(nativeCompiler(fs.readFileSync(path.join(parserSource, `${name}.bs`), 'utf8'))) + } + const example = 'func greet():\n print("native compiler ready")\nend\ngreet()' + const messages = [] + new Script(nativeCompiler(example)).runInNewContext({ console: { log: message => messages.push(message) } }) + if (messages.length !== 1 || messages[0] !== 'native compiler ready') { + throw new Error('Generated compiler failed its bootstrap smoke test') + } for (const name of fs.readdirSync(source, { withFileTypes: true })) { if (name.isFile() && name.name.endsWith('.bs')) { fs.writeFileSync(path.join(temporary, name.name.replace(/\.bs$/, '.js')), - compileSource(fs.readFileSync(path.join(source, name.name), 'utf8'))) + nativeCompiler(fs.readFileSync(path.join(source, name.name), 'utf8'))) } } @@ -24,9 +43,19 @@ async function main() { format: 'cjs', bundle: true, minify: true, + banner: { js: '#!/usr/bin/env node' }, target: 'node20' }) fs.chmodSync(output, 0o755) + await build({ + entryPoints: [path.join(root, 'scripts', 'browser-entry.cjs')], + outfile: path.join(root, 'js', 'browser.js'), + platform: 'browser', + format: 'iife', + bundle: true, + minify: true, + alias: { 'bynix-native': path.join(parserOutput, 'compiler.js') } + }) } finally { fs.rmSync(temporary, { recursive: true, force: true }) } diff --git a/scripts/compile.cjs b/scripts/compile.cjs index 853c895..b568115 100644 --- a/scripts/compile.cjs +++ b/scripts/compile.cjs @@ -1,5 +1,9 @@ const { parsing, parsingMsg, parseCode, addSemicolons } = require('bynixscript/js/parser.js') const replacements = require('bynixscript/dist/parser/parsingDecl.js') +const bootstrapVersion = '0.2.1-next' +if (require('bynixscript/package.json').version !== bootstrapVersion) { + throw new Error(`Bootstrap compiler must be bynixscript@${bootstrapVersion}`) +} const passes = [ 'flowReplace', 'utilityReplace', 'funcReplace', 'condReplace', @@ -10,14 +14,20 @@ const passes = [ ] function compileSource(source) { - let code = addSemicolons(parseCode(source)) + const literals = [] + const protectedSource = source.replace(/(?:`(?:\\[\s\S]|[^`])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g, literal => { + const placeholder = `__BYNIX_LITERAL_${literals.length}__` + literals.push(literal) + return placeholder + }) + let code = addSemicolons(parseCode(protectedSource)) if (parsing(code) === false) { throw new Error(parsingMsg.message) } for (const pass of passes) { code = replacements[pass](code) } - return code + return code.replace(/__BYNIX_LITERAL_(\d+)__/g, (_, index) => literals[Number(index)]) } module.exports = { compileSource } diff --git a/src/parser/assignDecl.bs b/src/parser/assignDecl.bs deleted file mode 100644 index eaa7912..0000000 --- a/src/parser/assignDecl.bs +++ /dev/null @@ -1,9 +0,0 @@ -func assignReplace(codeMsg): - codeMsg = codeMsg.change(/const\s([a-zA-Z_][a-zA-Z0-9_]*)\s=\s(.+?)/g, (match, p1, p2) => `const ${p1} = ${p2}`); - codeMsg = codeMsg.change(/const\s([a-zA-Z_][a-zA-Z0-9_]*)\s=\s(.+?);/g, (match, p1, p2) => `const ${p1} = ${p2};`); - codeMsg = codeMsg.change(/const\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?)/g, (match, p1, p2) => `const ${p1} = ${p2}`); - codeMsg = codeMsg.change(/const\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `const ${p1} = ${p2};`); - return codeMsg -end - -module.exports = { assignReplace } \ No newline at end of file diff --git a/src/parser/asyncDecl.bs b/src/parser/asyncDecl.bs deleted file mode 100644 index e228d37..0000000 --- a/src/parser/asyncDecl.bs +++ /dev/null @@ -1,9 +0,0 @@ -func asyncReplace(codeMsg): - codeMsg = codeMsg.change(/delay\:\n(.+?)/g, (match, p1) => `setTimeout(function() {\n ${p1}`); - codeMsg = codeMsg.change(/repeat\:\n(.+?)/g, (match, p1) => `setInterval(function() {\n ${p1}`); - codeMsg = codeMsg.change(/delay\:\s*(.+?)/g, (match, p1) => `setTimeout(function() {\n ${p1}`); - codeMsg = codeMsg.change(/repeat\:\s*(.+?)/g, (match, p1) => `setInterval(function() {\n ${p1}`); - return codeMsg -end - -module.exports = { asyncReplace } \ No newline at end of file diff --git a/src/parser/checkDecl.bs b/src/parser/checkDecl.bs deleted file mode 100644 index 1dc721e..0000000 --- a/src/parser/checkDecl.bs +++ /dev/null @@ -1,14 +0,0 @@ -func checkReplace(codeMsg): - codeMsg = codeMsg.change(/(? `typeof ${p1}`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_type\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1, p2) => `typeof ${p1} === ${p2}`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])type(["'])/g, (match, p1) => `${p1} instanceof TypeError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])reference(["'])/g, (match, p1) => `${p1} instanceof ReferenceError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])syntax(["'])/g, (match, p1) => `${p1} instanceof SyntaxError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])range(["'])/g, (match, p1) => `${p1} instanceof RangeError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])uri(["'])/g, (match, p1) => `${p1} instanceof URIError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])eval(["'])/g, (match, p1) => `${p1} instanceof EvalError`) - codeMsg = codeMsg.change(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])(.+?)(["'])/g, (match, p1, p2, p3) => `${p1} instanceof ${p3}`) - return codeMsg -end - -module.exports = { checkReplace } \ No newline at end of file diff --git a/src/parser/commentDecl.bs b/src/parser/commentDecl.bs deleted file mode 100644 index 32b1eb9..0000000 --- a/src/parser/commentDecl.bs +++ /dev/null @@ -1,8 +0,0 @@ -func commentReplace(codeMsg): - codeMsg = codeMsg.change(/(? `//${p1}`); - codeMsg = codeMsg.change(/\*\*(.+?)/g, (match, p1) => `/*${p1}`); - codeMsg = codeMsg.change(/(.+?)\*\*/g, (match, p1) => `${p1}*/`); - return codeMsg -end - -module.exports = { commentReplace } \ No newline at end of file diff --git a/src/parser/compiler.bs b/src/parser/compiler.bs new file mode 100644 index 0000000..cb3118d --- /dev/null +++ b/src/parser/compiler.bs @@ -0,0 +1,173 @@ +const { parse } = require('./syntax.js') + +func unsupported(node, message): + throw new SyntaxError(`${message} at ${node.line}:${node.column}`) +end + +func emitExpression(node): + if node['type'] === 'literal': + if ['class', 'switch', 'async', 'await', 'import', 'export', 'yield', 'function'].includes(node.value): + unsupported(node, `Unsupported syntax ${node.value}`) + end + if node.value === 'Err': + return 'Error' + end + return node.value + elif node['type'] === 'member': + var property = node.property + if property === 'is_end': + property = 'endsWith' + elif property === 'is_start': + property = 'startsWith' + elif property === 'is_includes': + property = 'includes' + elif property === 'is_matched': + property = 'match' + elif property === 'text': + property = 'textContent' + elif property === 'design': + property = 'style' + elif property === 'addClass': + return `${emitExpression(node.object)}.classList.add` + elif property === 'addElement': + property = 'appendChild' + elif property === 'change': + property = 'replace' + elif property.slice(0, 3) === 'is_': + unsupported(node, `Unsupported property ${property}`) + end + return `${emitExpression(node.object)}${node.optional ? '?.' : '.'}${property}` + elif node['type'] === 'index': + return `${emitExpression(node.object)}[${emitExpression(node.property)}]` + elif node['type'] === 'call': + var callee = emitExpression(node.callee) + if node.callee['type'] === 'literal': + if node.callee.value === 'print': + callee = 'console.log' + elif node.callee.value === 'int': + callee = 'parseInt' + elif node.callee.value === 'num': + callee = 'Number' + elif node.callee.value === 'str': + callee = 'String' + elif node.callee.value === 'float': + callee = 'parseFloat' + elif node.callee.value === 'json': + callee = 'JSON.parse' + elif node.callee.value === 'touch': + callee = 'alert' + elif node.callee.value === 'ask': + callee = 'prompt' + elif node.callee.value === 'rand': + callee = 'Math.random' + elif node.callee.value === 'roundDown': + callee = 'Math.floor' + elif node.callee.value === 'roundUp': + callee = 'Math.ceil' + elif node.callee.value === 'maxOf': + callee = 'Math.max' + elif node.callee.value === 'minOf': + callee = 'Math.min' + elif node.callee.value === 'getElement': + callee = 'document.querySelector' + elif node.callee.value === 'getAllElement': + callee = 'document.querySelectorAll' + elif node.callee.value === 'createElement': + callee = 'document.createElement' + end + elif node.callee['type'] === 'member' && node.callee.object.value === 'print': + if ['err', 'info', 'warn', 'debug', 'trace', 'assert'].includes(node.callee.property): + callee = `console.${node.callee.property === 'err' ? 'error' : node.callee.property}` + end + end + const args = [] + for const arg of node.args: + args.push(emitExpression(arg)) + end + return `${callee}(${args.join(', ')})` + elif node['type'] === 'binary': + return `(${emitExpression(node.left)} ${node.operator} ${emitExpression(node.right)})` + elif node['type'] === 'unary': + return `${node.operator}${['new', 'typeof', 'void', 'delete'].includes(node.operator) ? ' ' : ''}${emitExpression(node.argument)}` + elif node['type'] === 'postfix': + return `${emitExpression(node.argument)}${node.operator}` + elif node['type'] === 'conditional': + return `(${emitExpression(node.condition)} ? ${emitExpression(node.yes)} : ${emitExpression(node.no)})` + elif node['type'] === 'group': + return `(${emitExpression(node.value)})` + elif node['type'] === 'array': + const items = [] + for const item of node.items: + items.push(emitExpression(item)) + end + return `[${items.join(', ')}]` + elif node['type'] === 'object': + const entries = [] + for const entry of node.entries: + if entry.value === null: + entries.push(entry.key) + else: + entries.push(`${entry.key}: ${emitExpression(entry.value)}`) + end + end + return `{ ${entries.join(', ')} }` + end + unsupported(node, `Unsupported expression ${node['type']}`) +end + +func emitBlock(body): + const output = [] + for const node of body: + output.push(emitStatement(node)) + end + return output.join('\n') +end + +func emitStatement(node): + if node['type'] === 'function': + return `function ${node.name}(${node.parameters.join(', ')}) {\n${emitBlock(node.body)}\n}` + elif node['type'] === 'if': + const output = [] + for const branch of node.branches: + output.push(`${output.length === 0 ? 'if' : 'else if'} (${emitExpression(branch.condition)}) {\n${emitBlock(branch.body)}\n}`) + end + if node.otherwise !== null: + output.push(`else {\n${emitBlock(node.otherwise)}\n}`) + end + return output.join(' ') + elif node['type'] === 'for': + return `for (${node.declaration === null ? '' : node.declaration + ' '}${emitExpression(node.binding)} of ${emitExpression(node.iterable)}) {\n${emitBlock(node.body)}\n}` + elif node['type'] === 'while': + return `while (${emitExpression(node.condition)}) {\n${emitBlock(node.body)}\n}` + elif node['type'] === 'try': + if node.recovery === null && node.final === null: + unsupported(node, 'A recovery or final block is required') + end + var output = `try {\n${emitBlock(node.body)}\n}` + if node.recovery !== null: + output += ` catch (${node.recovery.name}) {\n${emitBlock(node.recovery.body)}\n}` + end + if node.final !== null: + output += ` finally {\n${emitBlock(node.final)}\n}` + end + return output + elif node['type'] === 'declaration': + if node.value === null && node.kind === 'const': + unsupported(node, 'A constant requires an initializer') + end + return `${node.kind} ${emitExpression(node.name)}${node.value === null ? '' : ' = ' + emitExpression(node.value)};` + elif node['type'] === 'return' || node['type'] === 'throw': + return `${node['type']}${node.value === null ? '' : ' ' + emitExpression(node.value)};` + elif node['type'] === 'break' || node['type'] === 'continue': + return `${node['type']};` + elif node['type'] === 'expression': + return `${emitExpression(node.value)};` + end + unsupported(node, `Unsupported statement ${node['type']}`) +end + +func compileSource(source): + return emitBlock(parse(source).body) +end + +module.exports = { compileSource, emitExpression, emitStatement } diff --git a/src/parser/condDecl.bs b/src/parser/condDecl.bs deleted file mode 100644 index 5ecf6c4..0000000 --- a/src/parser/condDecl.bs +++ /dev/null @@ -1,11 +0,0 @@ -func condReplace(codeMsg): - codeMsg = codeMsg.change(/elif\s(.+?):/g, (match, p1) => `} else if (${p1}) {`); - codeMsg = codeMsg.change(/if\s(.+?)\:/g, (match, p1) => `if (${p1}) {`); - codeMsg = codeMsg.change(/else\:/g, "} else {"); - codeMsg = codeMsg.change(/elif\s*(.+?):/g, (match, p1) => `} else if (${p1}) {`); - codeMsg = codeMsg.change(/if\s*(.+?)\:/g, (match, p1) => `if (${p1}) {`); - codeMsg = codeMsg.change(/else\:/g, "} else {"); - return codeMsg -end - -module.exports = { condReplace } \ No newline at end of file diff --git a/src/parser/convDecl.bs b/src/parser/convDecl.bs deleted file mode 100644 index e97add3..0000000 --- a/src/parser/convDecl.bs +++ /dev/null @@ -1,16 +0,0 @@ -func convReplace(codeMsg): - codeMsg = codeMsg.change(/int\((.+?)\)/g, (match, p1) => `parseInt(${p1})`) - codeMsg = codeMsg.change(/num\((.+?)\)/g, (match, p1) => `Number(${p1})`) - codeMsg = codeMsg.change(/str\((.+?)\)/g, (match, p1) => `String(${p1})`) - codeMsg = codeMsg.change(/float\((.+?)\)/g, (match, p1) => `parseFloat(${p1})`) - codeMsg = codeMsg.change(/json\((.+?)\)/g, (match, p1) => `JSON.parse(${p1})`) - codeMsg = codeMsg.change(/(.+?)\.map\((.+?)\)\:/g, (match, p1, p2) => `${p1}.map(function(${p2}) {`) - codeMsg = codeMsg.change(/\.map\((.+?)\)/g, (match, p1) => `.map(${p1})`) - codeMsg = codeMsg.change(/(.+?)\.filter\((.+?)\)\:/g, (match, p1, p2) => `${p1}.filter(function(${p2}) {`) - codeMsg = codeMsg.change(/\.filter\((.+?)\)/g, (match, p1) => `.filter(${p1})`) - codeMsg = codeMsg.change(/(.+?)\.reduce\((.+?)\)\:/g, (match, p1, p2) => `${p1}.reduce(function(${p2}) {`) - codeMsg = codeMsg.change(/\.reduce\((.+?)\)/g, (match, p1) => `.reduce(${p1})`) - return codeMsg -end - -module.exports = { convReplace } \ No newline at end of file diff --git a/src/parser/domDecl.bs b/src/parser/domDecl.bs deleted file mode 100644 index 031bd66..0000000 --- a/src/parser/domDecl.bs +++ /dev/null @@ -1,17 +0,0 @@ -func domReplace(codeMsg): - codeMsg = codeMsg.change(/([^\s].+?)\.image\s*=\s*(['"`][^'"`]*['"``])/g, (match, p1, p2) => `image(${p1}, ${p2})`) - codeMsg = codeMsg.change(/getElement\((.+?)\)/g, (match, p1) => `document.querySelector(${p1})`) - codeMsg = codeMsg.change(/getAllElement\((.+?)\)/g, (match, p1) => `document.querySelectorAll(${p1})`) - codeMsg = codeMsg.change(/createElement\((.+?)\)/g, (match, p1) => `document.createElement(${p1})`) - codeMsg = codeMsg.change(/([^\s].+)\.addElement\((.+?)\)/g, (match, p1, p2) => `document.${p1}.appendChild(${p2})`) - codeMsg = codeMsg.change(/(.+?)\.listener\((.+?)\)\:/g, (match, p1, p2) => `${p1}.addEventListener(${p2}, () => {`) - codeMsg = codeMsg.change(/listener\((.+?)\)\:/g, (match, p1) => `document.addEventListener(${p1}, function() {`) - codeMsg = codeMsg.change(/\.addClass\((.+?)\)/g, (match, p1) => `.classList.add(${p1})`) - codeMsg = codeMsg.change(/(.+?)\.change\((.+?), \((.+?)\)\)\:/g, (match, p1, p2, p3) => `${p1}.replace(${p2}, (${p3}) => {`) - codeMsg = codeMsg.change(/(.+?)\.change\((.+)\)/g, (match, p1, p2) => `${p1}.replace(${p2})`) - codeMsg = codeMsg.change(/(.+?)\.text/g, (match, p1) => `${p1}.textContent`) - codeMsg = codeMsg.change(/(.+?)\.text = (.+?)/g, (match, p1, p2) => `${p1}.textContent = ${p2}`) - return codeMsg -end - -module.exports = { domReplace } \ No newline at end of file diff --git a/src/parser/excepDecl.bs b/src/parser/excepDecl.bs deleted file mode 100644 index 3a51ade..0000000 --- a/src/parser/excepDecl.bs +++ /dev/null @@ -1,19 +0,0 @@ -func excepReplace(codeMsg): - codeMsg = codeMsg.change(/handle\:/g, 'try {') - codeMsg = codeMsg.change(/recovery\s\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - codeMsg = codeMsg.change(/final\:/g, "} finally {") - codeMsg = codeMsg.change(/handle\:/g, 'try {') - codeMsg = codeMsg.change(/recovery\s*\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - codeMsg = codeMsg.change(/final\:/g, "} finally {") - codeMsg = codeMsg.change(/Err\((.+?)\)/g, (match, p1) => `Error(${p1})`) - codeMsg = codeMsg.change(/URIErr\((.+?)\)/g, (match, p1) => `URIError(${p1})`) - codeMsg = codeMsg.change(/EvalErr\((.+?)\)/g, (match, p1) => `EvalError(${p1})`) - codeMsg = codeMsg.change(/TypeErr\((.+?)\)/g, (match, p1) => `TypeError(${p1})`) - codeMsg = codeMsg.change(/RangeErr\((.+?)\)/g, (match, p1) => `RangeError(${p1})`) - codeMsg = codeMsg.change(/SyntaxErr\((.+?)\)/g, (match, p1) => `Error(${p1})`) - codeMsg = codeMsg.change(/InternalErr\((.+?)\)/g, (match, p1) => `InternalError(${p1})`) - codeMsg = codeMsg.change(/ReferenceErr\((.+?)\)/g, (match, p1) => `ReferenceError(${p1})`) - return codeMsg -end - -module.exports = { excepReplace } \ No newline at end of file diff --git a/src/parser/expDecl.bs b/src/parser/expDecl.bs deleted file mode 100644 index b654d30..0000000 --- a/src/parser/expDecl.bs +++ /dev/null @@ -1,6 +0,0 @@ -func expReplace(codeMsg): - codeMsg = codeMsg.change(/(.+?)\.change\((.+?), \((.+?)\)\)\:/g, (match, p1, p2, p3) => `${p1}.replace(${p2}, (${p3}) => {`) - codeMsg = codeMsg.change(/(.+?)\.change\((.+)\)/g, (match, p1, p2) => `${p1}.replace(${p2})`); -end - -module.exports = { expReplace } \ No newline at end of file diff --git a/src/parser/flowDecl.bs b/src/parser/flowDecl.bs deleted file mode 100644 index 5d75a77..0000000 --- a/src/parser/flowDecl.bs +++ /dev/null @@ -1,9 +0,0 @@ -func flowReplace(codeMsg): - codeMsg = codeMsg.change(/(? `switch (${p1}) {`) - codeMsg = codeMsg.change("switch ((", "switch (") - codeMsg = codeMsg.change(")) {", ") {") - codeMsg = codeMsg.change(/case (.+?):\n(.+?)/g, (match, p1, p2) => `case ${p1}: \n${p2}`) - return codeMsg -end - -module.exports = { flowReplace } \ No newline at end of file diff --git a/src/parser/forDecl.bs b/src/parser/forDecl.bs deleted file mode 100644 index 07d0d96..0000000 --- a/src/parser/forDecl.bs +++ /dev/null @@ -1,8 +0,0 @@ -func forReplace(codeMsg): - codeMsg = codeMsg.change(/for\s(.+?)=(.+?)\sto\s(.+?)\:/g, (match, p1, p2, p3) => `for (let ${p1} = ${p2}; ${p1} < ${p3}; ${p1}++) {`); - codeMsg = codeMsg.change(/for\s(.+?)\sof\s(.+?)\:/g, (match, p1, p2) => `for (${p1} of ${p2}) {`) - codeMsg = codeMsg.change(/for\s(.+?)\sin\s(.+?)\:/g, (match, p1, p2) => `for (${p1} in ${p2}) {`) - return codeMsg -end - -module.exports = { forReplace } \ No newline at end of file diff --git a/src/parser/forEachDecl.bs b/src/parser/forEachDecl.bs deleted file mode 100644 index 9632451..0000000 --- a/src/parser/forEachDecl.bs +++ /dev/null @@ -1,7 +0,0 @@ -func forEachReplace(codeMsg): - codeMsg = codeMsg.change(/forEach\((.+?)\)\:/g, (match, p1) => `forEach(${p1} => {`); - codeMsg = codeMsg.change(/forEach\((.+?)\s=>\s(.+?)\)/g, (match, p1, p2) => `forEach(${p1} => ${p2})`); - return codeMsg -end - -module.exports = { forEachReplace } \ No newline at end of file diff --git a/src/parser/funcDecl.bs b/src/parser/funcDecl.bs deleted file mode 100644 index c09e917..0000000 --- a/src/parser/funcDecl.bs +++ /dev/null @@ -1,6 +0,0 @@ -func funcReplace(codeMsg): - codeMsg = codeMsg.change(/func\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\):/g, (match, p1, p2) => `function ${p1}(${p2}) {`); - return codeMsg -end - -module.exports = { funcReplace } \ No newline at end of file diff --git a/src/parser/interactDecl.bs b/src/parser/interactDecl.bs deleted file mode 100644 index d6fce69..0000000 --- a/src/parser/interactDecl.bs +++ /dev/null @@ -1,8 +0,0 @@ -func interactReplace(codeMsg): - codeMsg = codeMsg.change(/touch\((.+?)\)/g, (match, p1) => `alert(${p1})`) - codeMsg = codeMsg.change(/ask\((.+?)\)/g, (match, p1) => `prompt(${p1})`) - codeMsg = codeMsg.change(/confirm\((.+?)\)/g, (match, p1) => `confirm(${p1})`) - return codeMsg -end - -module.exports = { interactReplace } \ No newline at end of file diff --git a/src/parser/lexer.bs b/src/parser/lexer.bs new file mode 100644 index 0000000..7cb1202 --- /dev/null +++ b/src/parser/lexer.bs @@ -0,0 +1,131 @@ +func lex(source): + const identifierStart = new RegExp('^[a-zA-Z_$]$') + const identifierPart = new RegExp('^[a-zA-Z0-9_$]$') + const digit = new RegExp('^[0-9]$') + const numberPattern = new RegExp('^[0-9]+(\\.[0-9]+)?$') + const tokens = [] + var offset = 0 + var line = 1 + var column = 1 + var depth = 0 + while (offset < source.length) { + const start = offset + const row = line + const col = column + const character = source[offset] + if character === ' ' || character === '\t' || character === '\r': + offset += 1 + column += 1 + elif character === '\n': + if depth === 0: + tokens.push({ kind: 'newline', value: '\n', line: row, column: col }) + end + offset += 1 + line += 1 + column = 1 + elif character === '#' || (character === '/' && source[offset + 1] === '/'): + while (offset < source.length && source[offset] !== '\n') { + offset += 1 + column += 1 + } + elif character === '/' && source[offset + 1] === '*': + offset += 2 + column += 2 + var closed = false + while (offset < source.length) { + if source[offset] === '*' && source[offset + 1] === '/': + offset += 2 + column += 2 + closed = true + break + end + if source[offset] === '\n': + line += 1 + column = 1 + else: + column += 1 + end + offset += 1 + } + if !closed: + throw new SyntaxError(`Unterminated comment at ${row}:${col}`) + end + elif character === '"' || character === "'" || character === '`': + const quote = character + offset += 1 + column += 1 + var closed = false + while (offset < source.length) { + if source[offset] === '\\': + if offset + 1 >= source.length: + break + end + offset += 2 + column += 2 + elif source[offset] === quote: + offset += 1 + column += 1 + closed = true + break + elif source[offset] === '\n': + if quote !== '`': + throw new SyntaxError(`Unterminated string at ${row}:${col}`) + end + offset += 1 + line += 1 + column = 1 + else: + offset += 1 + column += 1 + end + } + if !closed: + throw new SyntaxError(`Unterminated string at ${row}:${col}`) + end + tokens.push({ kind: 'string', value: source.slice(start, offset), line: row, column: col }) + elif identifierStart.test(character): + while (offset < source.length && identifierPart.test(source[offset]) === true) { + offset += 1 + column += 1 + } + tokens.push({ kind: 'identifier', value: source.slice(start, offset), line: row, column: col }) + elif digit.test(character): + while (offset < source.length && (digit.test(source[offset]) || source[offset] === '.') === true) { + offset += 1 + column += 1 + } + const value = source.slice(start, offset) + if !numberPattern.test(value): + throw new SyntaxError(`Invalid number at ${row}:${col}`) + end + tokens.push({ kind: 'number', value, line: row, column: col }) + else: + const triple = source.slice(offset, offset + 3) + const double = source.slice(offset, offset + 2) + var value = character + if ['===', '!==', '**=', '...', '>>>'].includes(triple): + value = triple + elif ['==', '!=', '<=', '>=', '&&', '||', '??', '+=', '-=', '*=', '/=', '++', '--', '**', '=>', '?.'].includes(double): + value = double + end + if !'()[]{}.,:;?+-*/%!=<>|&'.includes(character): + throw new SyntaxError(`Unexpected character ${character} at ${row}:${col}`) + end + offset += value.length + column += value.length + if value === '(' || value === '[': + depth += 1 + elif value === ')' || value === ']': + depth -= 1 + if depth < 0: + throw new SyntaxError(`Unexpected ${value} at ${row}:${col}`) + end + end + tokens.push({ kind: 'symbol', value, line: row, column: col }) + end + } + tokens.push({ kind: 'eof', value: '', line, column }) + return tokens +end + +module.exports = { lex } diff --git a/src/parser/logDecl.bs b/src/parser/logDecl.bs deleted file mode 100644 index 494a56e..0000000 --- a/src/parser/logDecl.bs +++ /dev/null @@ -1,12 +0,0 @@ -func logReplace(codeMsg): - codeMsg = codeMsg.change(/print\((.+?)\)/g, (match, p1) => `console.log(${p1})`) - codeMsg = codeMsg.change(/print\.err\((.+?)\)/g, (match, p1) => `console.error(${p1})`) - codeMsg = codeMsg.change(/print\.info\((.+?)\)/g, (match, p1) => `console.info(${p1})`) - codeMsg = codeMsg.change(/print\.warn\((.+?)\)/g, (match, p1) => `console.warn(${p1})`) - codeMsg = codeMsg.change(/print\.debug\((.+?)\)/g, (match, p1) => `console.debug(${p1})`) - codeMsg = codeMsg.change(/print\.trace\((.+?)\)/g, (match, p1) => `console.trace(${p1})`) - codeMsg = codeMsg.change(/print\.assert\((.+?)\)/g, (match, p1) => `console.assert(${p1})`) - return codeMsg -end - -module.exports = { logReplace } \ No newline at end of file diff --git a/src/parser/mathDecl.bs b/src/parser/mathDecl.bs deleted file mode 100644 index ee76a5b..0000000 --- a/src/parser/mathDecl.bs +++ /dev/null @@ -1,10 +0,0 @@ -func mathReplace(codeMsg): - codeMsg = codeMsg.change(/rand\((.+?)\)/g, (match, p1) => `Math.random(${p1})`); - codeMsg = codeMsg.change(/roundDown\((.+?)\)/g, (match, p1) => `Math.floor(${p1})`); - codeMsg = codeMsg.change(/roundUp\((.+?)\)/g, (match, p1) => `Math.ceil(${p1})`); - codeMsg = codeMsg.change(/maxOf\((.+?)\)/g, (match, p1) => `Math.min(${p1})`); - codeMsg = codeMsg.change(/minOf\((.+?)\)/g, (match, p1) => `Math.max(${p1})`); - return codeMsg -end - -module.exports = { mathReplace } \ No newline at end of file diff --git a/src/parser/oopDecl.bs b/src/parser/oopDecl.bs deleted file mode 100644 index 29d0fd8..0000000 --- a/src/parser/oopDecl.bs +++ /dev/null @@ -1,13 +0,0 @@ -func oopReplace(codeMsg): - codeMsg = codeMsg.change(/constructor\((.+?)\)\:/g, (match, p1) => `constructor(${p1}) {`); - codeMsg = codeMsg.change(/(.+?)\(\)\:/g, (match, p1) => `${p1}() {`); - codeMsg = codeMsg.change(/class\s(.+?)\:/g, (match, p1) => `class ${p1} {`); - codeMsg = codeMsg.change(/class\s(.+?)\sextends\s(.+?)\:/g, (match, p1, p2) => `class ${p1} extends ${p2} {`); - codeMsg = codeMsg.change(/static\((.+?)\)\:/g, (match, p1) => `static(${p1}) {`); - codeMsg = codeMsg.change(/super\((.+?)\)\:/g, (match, p1) => `super(${p1})`) - codeMsg = codeMsg.change(/throw (.+?)/g, (match, p1) => `throw ${p1}`) - codeMsg = codeMsg.change(/new (.+?)/g, (match, p1) => `new ${p1}`) - return codeMsg -end - -module.exports = { oopReplace } \ No newline at end of file diff --git a/src/parser/otherDecl.bs b/src/parser/otherDecl.bs deleted file mode 100644 index 86a3f07..0000000 --- a/src/parser/otherDecl.bs +++ /dev/null @@ -1,7 +0,0 @@ -func otherReplace(code): - code = code.change(/process\.argv\[(\d+)\]/g, (match, p1) => `process.argv[${a}]`); - codeMsg = codeMsg.change(/end\:(.+?)/g, (match, p1) => `}, ${p1});`); - return codeMsg -end - -module.exports = { otherReplace } \ No newline at end of file diff --git a/src/parser/parsingDecl.bs b/src/parser/parsingDecl.bs deleted file mode 100644 index 6ae1302..0000000 --- a/src/parser/parsingDecl.bs +++ /dev/null @@ -1,22 +0,0 @@ -const { funcReplace } = require('./funcDecl.js') -const { condReplace } = require('./condDecl.js') -const { forEachReplace } = require('./forEachDecl.js') -const { reassignReplace } = require('./reassignDecl.js') -const { assignReplace } = require('./assignDecl.js') -const { logReplace } = require('./logDecl.js') -const { interactReplace } = require('./interactDecl.js') -const { mathReplace } = require('./mathDecl.js') -const { domReplace } = require('./domDecl.js') -const { commentReplace } = require('./commentDecl.js') -const { utilityReplace } = require('./utilityDecl.js') -const { asyncReplace } = require('./asyncDecl') -const { flowReplace } = require('./flowDecl.js') -const { forReplace } = require('./forDecl.js') -const { convReplace } = require('./convDecl.js') -const { checkReplace } = require('./checkDecl.js') -const { oopReplace } = require('./oopDecl.js') -const { excepReplace } = require('./excepDecl.js') -const { expReplace } = require('./expDecl.js') -const { otherReplace } = require('./otherDecl.js') - -module.exports = { funcReplace, condReplace, forEachReplace, reassignReplace, assignReplace, logReplace, interactReplace, mathReplace, domReplace, commentReplace, utilityReplace, asyncReplace, flowReplace, forReplace, convReplace, checkReplace, oopReplace, excepReplace, expReplace, otherReplace } \ No newline at end of file diff --git a/src/parser/reassignDecl.bs b/src/parser/reassignDecl.bs deleted file mode 100644 index 5af5deb..0000000 --- a/src/parser/reassignDecl.bs +++ /dev/null @@ -1,6 +0,0 @@ -func reassignReplace(codeMsg): - codeMsg = codeMsg.change(/var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `var ${p1} = ${p2};`); - return codeMsg -end - -module.exports = { reassignReplace } \ No newline at end of file diff --git a/src/parser/syntax.bs b/src/parser/syntax.bs new file mode 100644 index 0000000..672c0fc --- /dev/null +++ b/src/parser/syntax.bs @@ -0,0 +1,297 @@ +const { lex } = require('./lexer.js') + +func current(state): + return state.tokens[state.index] +end + +func fail(state, message): + const token = current(state) + throw new SyntaxError(`${message} at ${token.line}:${token.column}`) +end + +func accept(state, value): + if current(state).value === value: + state.index += 1 + return true + end + return false +end + +func expect(state, value): + if !accept(state, value): + fail(state, `Expected ${value}`) + end +end + +func readName(state): + const token = current(state) + if token.kind !== 'identifier': + fail(state, 'Expected identifier') + end + state.index += 1 + return token.value +end + +func separators(state): + while (current(state).kind === 'newline' || current(state).value === ';') { + state.index += 1 + } +end + +func precedence(operator): + const levels = [['=', '+=', '-=', '*=', '/='], ['||', '??'], ['&&'], ['==', '!=', '===', '!=='], ['<', '>', '<=', '>=', 'in', 'instanceof'], ['+', '-'], ['*', '/', '%'], ['**']] + var index = 0 + while (index < levels.length) { + if levels[index].includes(operator): + return index + 1 + end + index += 1 + } + return 0 +end + +func expression(state, minimum): + var left = primary(state) + while (true) { + const token = current(state) + const operator = token.value + if operator === '.' || operator === '?.': + state.index += 1 + left = { type: 'member', object: left, property: readName(state), optional: operator === '?.', line: token.line, column: token.column } + elif operator === '[': + state.index += 1 + const property = expression(state, 1) + expect(state, ']') + left = { type: 'index', object: left, property, line: token.line, column: token.column } + elif operator === '(': + state.index += 1 + const args = [] + if !accept(state, ')'): + while (true) { + args.push(expression(state, 1)) + if !accept(state, ','): + break + end + } + expect(state, ')') + end + left = { type: 'call', callee: left, args, line: token.line, column: token.column } + elif operator === '++' || operator === '--': + state.index += 1 + left = { type: 'postfix', operator, argument: left, line: token.line, column: token.column } + elif operator === '?' && minimum <= 2: + state.index += 1 + const yes = expression(state, 1) + expect(state, ':') + left = { type: 'conditional', condition: left, yes, no: expression(state, 2), line: token.line, column: token.column } + else: + const rank = precedence(operator) + if rank === 0 || rank < minimum: + break + end + state.index += 1 + const right = expression(state, rank + (rank === 1 || operator === '**' ? 0 : 1)) + left = { type: 'binary', operator, left, right, line: token.line, column: token.column } + end + } + return left +end + +func primary(state): + const token = current(state) + state.index += 1 + if token.kind === 'string' || token.kind === 'number' || token.kind === 'identifier' && !['new', 'typeof', 'void', 'delete'].includes(token.value): + return { type: 'literal', value: token.value, line: token.line, column: token.column } + elif ['!', '-', '+', 'typeof', 'void', 'delete', 'new'].includes(token.value): + return { type: 'unary', operator: token.value, argument: expression(state, token.value === 'new' ? 9 : 8), line: token.line, column: token.column } + elif token.value === '(': + const value = expression(state, 1) + expect(state, ')') + return { type: 'group', value, line: token.line, column: token.column } + elif token.value === '[': + const items = [] + if !accept(state, ']'): + while (true) { + items.push(expression(state, 1)) + if !accept(state, ','): + break + end + } + expect(state, ']') + end + return { type: 'array', items, line: token.line, column: token.column } + elif token.value === '{': + const entries = [] + if !accept(state, '}'): + while (true) { + const key = current(state) + if key.kind !== 'identifier' && key.kind !== 'string' && key.kind !== 'number': + fail(state, 'Expected property name') + end + state.index += 1 + var value = null + if accept(state, ':'): + value = expression(state, 1) + end + entries.push({ key: key.value, value }) + if !accept(state, ','): + break + end + } + expect(state, '}') + end + return { type: 'object', entries, line: token.line, column: token.column } + end + state.index -= 1 + fail(state, 'Expected expression') +end + +func block(state, stops): + const body = [] + separators(state) + while (current(state).kind !== 'eof' && !stops.includes(current(state).value) === true) { + body.push(statement(state)) + if current(state).kind !== 'eof' && !stops.includes(current(state).value) && current(state).kind !== 'newline' && current(state).value !== ';': + fail(state, 'Expected end of statement') + end + separators(state) + } + return body +end + +func colon(state): + expect(state, ':') + if current(state).kind !== 'newline' && current(state).kind !== 'eof': + fail(state, 'Expected newline after block header') + end +end + +func statement(state): + const token = current(state) + const word = token.value + state.index += 1 + if ['match', 'case', 'delay', 'repeat', 'class', 'constructor', 'static', 'async', 'listener', 'forEach'].includes(word): + state.index -= 1 + fail(state, `Unsupported syntax ${word}`) + end + if ['end', 'elif', 'else', 'recovery', 'final'].includes(word): + state.index -= 1 + fail(state, `Unexpected ${word}`) + end + if word === 'func': + const name = readName(state) + expect(state, '(') + const parameters = [] + if !accept(state, ')'): + while (true) { + parameters.push(readName(state)) + if !accept(state, ','): + break + end + } + expect(state, ')') + end + colon(state) + const body = block(state, ['end']) + expect(state, 'end') + return { type: 'function', name, parameters, body, line: token.line, column: token.column } + elif word === 'if': + const branches = [] + const condition = expression(state, 1) + colon(state) + branches.push({ condition, body: block(state, ['elif', 'else', 'end']) }) + while (accept(state, 'elif') === true) { + const next = expression(state, 1) + colon(state) + branches.push({ condition: next, body: block(state, ['elif', 'else', 'end']) }) + } + var otherwise = null + if accept(state, 'else'): + colon(state) + otherwise = block(state, ['end']) + end + expect(state, 'end') + return { type: 'if', branches, otherwise, line: token.line, column: token.column } + elif word === 'for': + var declaration = null + if ['const', 'var', 'let'].includes(current(state).value): + declaration = current(state).value + state.index += 1 + end + const binding = expression(state, 6) + if !accept(state, 'of'): + fail(state, 'Only for-of loops are supported') + end + const iterable = expression(state, 1) + colon(state) + const body = block(state, ['end']) + expect(state, 'end') + return { type: 'for', declaration, binding, iterable, body, line: token.line, column: token.column } + elif word === 'while': + var condition + var body + if accept(state, '('): + condition = expression(state, 1) + expect(state, ')') + expect(state, '{') + body = block(state, ['}']) + expect(state, '}') + else: + condition = expression(state, 1) + colon(state) + body = block(state, ['end']) + expect(state, 'end') + end + return { type: 'while', condition, body, line: token.line, column: token.column } + elif word === 'handle': + colon(state) + const body = block(state, ['recovery', 'final', 'end']) + var caught = null + var finallyBlock = null + if accept(state, 'recovery'): + expect(state, '(') + const name = readName(state) + expect(state, ')') + colon(state) + caught = { name, body: block(state, ['final', 'end']) } + end + if accept(state, 'final'): + colon(state) + finallyBlock = block(state, ['end']) + end + expect(state, 'end') + return { type: 'try', body, recovery: caught, 'final': finallyBlock, line: token.line, column: token.column } + elif word === 'const' || word === 'var' || word === 'let': + const name = expression(state, 2) + var value = null + if accept(state, '='): + value = expression(state, 1) + end + return { type: 'declaration', kind: word, name, value, line: token.line, column: token.column } + elif word === 'return' || word === 'throw': + var value = null + if current(state).kind !== 'newline' && current(state).kind !== 'eof' && current(state).value !== ';': + value = expression(state, 1) + end + if word === 'throw' && value === null: + fail(state, 'Expected expression') + end + return { type: word, value, line: token.line, column: token.column } + elif word === 'break' || word === 'continue': + return { type: word, line: token.line, column: token.column } + end + state.index -= 1 + const result = expression(state, 1) + return { type: 'expression', value: result, line: token.line, column: token.column } +end + +func parse(source): + const state = { tokens: lex(source), index: 0 } + const body = block(state, []) + if current(state).kind !== 'eof': + fail(state, 'Unexpected token') + end + return { type: 'program', body, line: 1, column: 1 } +end + +module.exports = { parse } diff --git a/src/parser/utilityDecl.bs b/src/parser/utilityDecl.bs deleted file mode 100644 index 2441503..0000000 --- a/src/parser/utilityDecl.bs +++ /dev/null @@ -1,21 +0,0 @@ -func utilityReplace(codeMsg): - codeMsg = codeMsg.change(/is_includes([\s\S]*?)/g, (match, p1) => `includes${p1}`); - codeMsg = codeMsg.change(/is_start\(([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)\)/g, (match, p1) => `startsWith(${p1})`); - codeMsg = codeMsg.change(/is_end\(([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)\)/g, (match, p1) => `endsWith(${p1})`); - codeMsg = codeMsg.change(/is_matched\((.+?)\)/g, (match, p1) => `match(${p1})`); - codeMsg = codeMsg.change(/is_value\(([a-zA-Z0-9"'`]+)\)/g, (match, p1) => `value === ${p1}`); - codeMsg = codeMsg.change(/is_design\(([a-zA-Z0-9"'`]+)\)/g, (match, p1) => `style === ${p1}`); - codeMsg = codeMsg.change(/(.+?)\.design\s=\s(.+?)/g, (match, p1, p2) => `${p1}.style = ${p2}`); - codeMsg = codeMsg.change(/is_action\s([=!><]+)\s(["'])checked(["'])/g, (match) => `checked`); - codeMsg = codeMsg.change(/is_action\s([=!><]+)\s(["'])selected(["'])/g, (match) => `selected`); - codeMsg = codeMsg.change(/is_action\s([=!><]+)\s(["'])open(["'])/g, (match) => `open`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])required(["'])/g, (match) => `required`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])readOnly(["'])/g, (match) => `readOnly`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])autofocus(["'])/g, (match) => `autofocus`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])disabled(["'])/g, (match) => `disabled`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])multiple(["'])/g, (match) => `multiple`); - codeMsg = codeMsg.change(/is_have\s([=!><]+)\s(["'])hidden(["'])/g, (match) => `hidden`); - return codeMsg -end - -module.exports = { utilityReplace } \ No newline at end of file diff --git a/src/translate.bs b/src/translate.bs index c2b7f65..7f3dbb3 100644 --- a/src/translate.bs +++ b/src/translate.bs @@ -1,8 +1,7 @@ const fs = require('node:fs') const path = require('node:path') const { extensions } = require('../js/bsc.js') -const { parsing, parsingMsg, parseCode, addSemicolons } = require('../js/parser.js') -const replacements = require('bynixscript/dist/parser/parsingDecl.js') +const { compileSource } = require('./parser/compiler.js') func resolveFile(file): if typeof file !== 'string' || file.trim() === '': @@ -27,15 +26,7 @@ func readSource(file): end func translate(source): - var code = addSemicolons(parseCode(source)) - if parsing(code) === false: - throw new Err(parsingMsg.message) - end - const passes = 'flowReplace utilityReplace funcReplace condReplace forEachReplace reassignReplace assignReplace logReplace interactReplace mathReplace commentReplace asyncReplace forReplace convReplace checkReplace oopReplace excepReplace domReplace'.split(' ') - for const pass of passes: - code = replacements[pass](code) - end - return code + return compileSource(source) end module.exports = { resolveFile, readSource, translate, extensions } diff --git a/tests/cli.test.cjs b/tests/cli.test.cjs index 8d85392..9640112 100644 --- a/tests/cli.test.cjs +++ b/tests/cli.test.cjs @@ -30,7 +30,7 @@ test('build produces the packaged executable', () => { assert.equal(invoke(['--version']).status, 0) }) -test('every CLI source compiles through the build compiler', () => { +test('bootstrap compiler accepts the CLI source subset', () => { for (const name of ['index', 'bsr', 'bst', 'bsp', 'bsd', 'translate']) { const source = fs.readFileSync(path.join(root, 'src', `${name}.bs`), 'utf8') const code = compileSource(source) diff --git a/tests/compiler.test.cjs b/tests/compiler.test.cjs new file mode 100644 index 0000000..5328caa --- /dev/null +++ b/tests/compiler.test.cjs @@ -0,0 +1,135 @@ +const test = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const vm = require('node:vm') +const { spawnSync } = require('node:child_process') +const { compileSource: bootstrapSource } = require('../scripts/compile.cjs') + +const root = path.resolve(__dirname, '..') +const names = ['lexer', 'syntax', 'compiler'] + +function sources() { + return names.map(name => fs.readFileSync(path.join(root, 'src/parser', `${name}.bs`), 'utf8')) +} + +function loadCompiler(context, translator) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'bynix-native-')) + context.after(() => fs.rmSync(directory, { recursive: true, force: true })) + for (const [index, name] of names.entries()) { + const output = translator(sources()[index]) + assert.doesNotThrow(() => new vm.Script(output, { filename: `${name}.js` })) + fs.writeFileSync(path.join(directory, `${name}.js`), output) + } + return { + lex: require(path.join(directory, 'lexer.js')).lex, + parse: require(path.join(directory, 'syntax.js')).parse, + compile: require(path.join(directory, 'compiler.js')).compileSource + } +} + +test('bootstrap version is exact in the manifest and installed package', () => { + const manifest = require('../package.json') + assert.equal(manifest.devDependencies.bynixscript, '0.2.1-next') + assert.equal(require('bynixscript/package.json').version, '0.2.1-next') +}) + +test('lexer tracks token locations and ignores keywords inside strings and comments', context => { + const { lex } = loadCompiler(context, bootstrapSource) + const tokens = lex('# note\nconst text = "if func end" // match\nprint(text)') + assert.deepEqual(tokens.slice(0, 5).map(token => [token.value, token.line, token.column]), [ + ['\n', 1, 7], ['const', 2, 1], ['text', 2, 7], ['=', 2, 12], ['"if func end"', 2, 14] + ]) + assert.equal(tokens.filter(token => token.value === 'match').length, 0) + assert.throws(() => lex('print("unfinished'), /Unterminated string at 1:7/) + assert.throws(() => lex('/* unfinished'), /Unterminated comment at 1:1/) +}) + +test('parser preserves precedence and nested block structure', context => { + const { parse, compile } = loadCompiler(context, bootstrapSource) + const source = 'func run():\n if true:\n const value = 2 + 3 * 4\n print(value)\n end\nend\nrun()' + const tree = parse(source) + assert.equal(tree.body[0].type, 'function') + assert.equal(tree.body[0].body[0].type, 'if') + const value = tree.body[0].body[0].branches[0].body[0].value + assert.equal(value.operator, '+') + assert.equal(value.right.operator, '*') + const output = [] + vm.runInNewContext(compile(source), { console: { log: item => output.push(item) } }) + assert.deepEqual(output, [14]) +}) + +test('generated code preserves strings, escaped text, and comments', context => { + const { compile } = loadCompiler(context, bootstrapSource) + const source = '/* func fake(): end */\nprint("if func end // \\"quoted\\"")\n// print("hidden")\n' + const output = [] + vm.runInNewContext(compile(source), { console: { log: item => output.push(item) } }) + assert.deepEqual(output, ['if func end // "quoted"']) +}) + +test('malformed input and unsupported features fail with precise positions', context => { + const { compile } = loadCompiler(context, bootstrapSource) + for (const [source, error] of [ + ['func bad():\n print(1)', /Expected end at 2:10/], + ['if true:\n print(1)\nend\nend', /Unexpected end at 4:1/], + ['print(1 + )', /Expected expression at 1:11/], + ['match value:\nend', /Unsupported syntax match at 1:1/], + ['print(item.is_type)', /Unsupported property is_type at 1:11/] + ]) assert.throws(() => compile(source), error) +}) + +test('self-hosted compiler agrees on fixtures and its own sources', context => { + const first = loadCompiler(context, bootstrapSource) + const second = loadCompiler(context, first.compile) + const fixtures = [ + 'const result = 2 + 3 * (4 - 1)\nprint(result)', + 'func greet(value):\n if value > 2:\n print("end elif")\n else:\n print("other")\n end\nend\ngreet(3)', + 'for const item of [1, 2]:\n print(item)\nend', + 'handle:\n throw new Err("oops")\nrecovery (error):\n print(error.message)\nend' + ] + for (const source of [...sources(), ...fixtures]) { + assert.equal(first.compile(source), second.compile(source)) + } + assert.match(first.compile('print("print(hello) if x:")'), /console\.log\("print\(hello\) if x:"\)/) +}) + +test('generated compiler accepts every CLI source', context => { + const { compile } = loadCompiler(context, bootstrapSource) + for (const name of ['index', 'bsr', 'bst', 'bsp', 'bsd', 'translate']) { + const source = fs.readFileSync(path.join(root, 'src', `${name}.bs`), 'utf8') + assert.doesNotThrow(() => new vm.Script(compile(source), { filename: `${name}.js` })) + } +}) + +test('CLI rejects malformed input without writing partial output', context => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'bynix-errors-')) + context.after(() => fs.rmSync(directory, { recursive: true, force: true })) + fs.writeFileSync(path.join(directory, 'broken.bys'), 'if true:\n print(1)') + const result = spawnSync(process.execPath, [path.join(root, 'dist/index.min.cjs'), 'compile', 'broken.bys'], { + cwd: directory, encoding: 'utf8' + }) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Expected end at 2:10/) + assert.equal(fs.existsSync(path.join(directory, 'broken.js')), false) +}) + +test('browser bundle uses the same compiler for inline sources', context => { + let ready + const messages = [] + const tag = { + textContent: 'print("browser")', + getAttribute: () => null, + remove: () => messages.push('removed') + } + const sandbox = { + document: { + addEventListener: (_, callback) => { ready = callback }, + querySelectorAll: () => [tag] + }, + console: { log: message => messages.push(message), error: error => { throw error } } + } + vm.runInNewContext(fs.readFileSync(path.join(root, 'js/browser.js'), 'utf8'), sandbox) + ready() + assert.deepEqual(messages, ['browser', 'removed']) +})