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'])
+})