don't be eval

This commit is contained in:
Shautvast 2024-12-06 23:02:01 +01:00
commit e94671c85e
20 changed files with 5984 additions and 0 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

194
.gitattributes vendored Normal file
View file

@ -0,0 +1,194 @@
## GITATTRIBUTES FOR WEB PROJECTS
#
# These settings are for any web project.
#
# Details per file setting:
# text These files should be normalized (i.e. convert CRLF to LF).
# binary These files are binary and should be left untouched.
#
# Note that binary is a macro for -text -diff.
######################################################################
## AUTO-DETECT
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto
## SOURCE CODE
*.bat text eol=crlf
*.coffee text
*.css text
*.htm text
*.html text
*.inc text
*.ini text
*.js text
*.json text
*.jsx text
*.less text
*.od text
*.onlydata text
*.php text
*.pl text
*.py text
*.rb text
*.sass text
*.scm text
*.scss text
*.sh text eol=lf
*.sql text
*.styl text
*.tag text
*.ts text
*.tsx text
*.xml text
*.xhtml text
## DOCKER
*.dockerignore text
Dockerfile text
## DOCUMENTATION
*.markdown text
*.md text
*.mdwn text
*.mdown text
*.mkd text
*.mkdn text
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
## TEMPLATES
*.dot text
*.ejs text
*.haml text
*.handlebars text
*.hbs text
*.hbt text
*.jade text
*.latte text
*.mustache text
*.njk text
*.phtml text
*.tmpl text
*.tpl text
*.twig text
## LINTERS
.babelrc text
.csslintrc text
.eslintrc text
.htmlhintrc text
.jscsrc text
.jshintrc text
.jshintignore text
.prettierrc text
.stylelintrc text
## CONFIGS
*.bowerrc text
*.cnf text
*.conf text
*.config text
.browserslistrc text
.editorconfig text
.gitattributes text
.gitconfig text
.gitignore text
.htaccess text
*.npmignore text
*.yaml text
*.yml text
browserslist text
Makefile text
makefile text
## HEROKU
Procfile text
.slugignore text
## GRAPHICS
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
*.svg text
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
## AUDIO
*.kar binary
*.m4a binary
*.mid binary
*.midi binary
*.mp3 binary
*.ogg binary
*.ra binary
## VIDEO
*.3gpp binary
*.3gp binary
*.as binary
*.asf binary
*.asx binary
*.fla binary
*.flv binary
*.m4v binary
*.mng binary
*.mov binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogv binary
*.swc binary
*.swf binary
*.webm binary
## ARCHIVES
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
## FONTS
*.ttf binary
*.eot binary
*.otf binary
*.woff binary
*.woff2 binary
## EXECUTABLES
*.exe binary
*.pyc binary

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Include your project-specific ignores in this file
# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
# Useful .gitignore templates: https://github.com/github/gitignore
node_modules
dist
.cache
.idea/

38
README.md Normal file
View file

@ -0,0 +1,38 @@
__Szpakowski__
An algorithmic tribute to the Polish artist [Wacław Szpakowski](https://de.wikipedia.org/wiki/Wac%C5%82aw_Szpakowski)
![Szpakowski's Artwork](szpakowski.png) ![Screenshot](screenshot.png)
at this moment still work in progress...
What is it?
* Think _Logo_, the 'turtle' language from the eighties.
* Because Szpakowski's drawings all follow the principle of the single line.
* it has the basic commands to move the turtle:
- start(x,y)
- go(distance)
- left(degrees)
- right(degrees)
* on top of that it has:
- pillars(number, length, shift, direction=DOWN): the turtle follows a pillary pattern
- number: the number of pillars (running back and forth)
- length: the distance per run
- shift: in-/decrease in length
- direction: UO|DOWN|BOTH
- moving_pillars(n, length, shift=0, direction=DOWN): the pillars move
- number: the number of pillars (running back and forth)
- length: the distance per run
- shift: shift per run
- direction: UP|DOWN
- staircase(number, size, direction = DOWN)
- number: the number of steps
- size: the step size
- direction: UP|DOWN
... more functions will follow
### Run
* npm run start

203
css/style.css Normal file
View file

@ -0,0 +1,203 @@
html {
color: #222;
font-size: 1em;
line-height: 1.4;
}
#console {
font: 13px monospace, sans-serif;
padding: 5px;
color: greenyellow;
background: black;
position: fixed;
right: 10px;
bottom: 0;
width: 30%;
height: 20em;
border: 2px solid darkgray;
border-radius: 10px;
z-index: 0;
}
#help{
background: black;
font: 13px monospace, sans-serif;
padding: 5px;
width: 90%;
color: greenyellow;
position: absolute;
z-index: 2;
}
#command_input {
background: black;
color: greenyellow;
outline: none;
width: 90%;
height: 20em;
resize: none;
}
.multiline {
border: none transparent;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.single_line {
border: none transparent;
}
.hidden,
[hidden] {
display: none !important;
}
/*
* Hide only visually, but have it available for screen readers:
* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
*
* 1. For long content, line feeds are not interpreted as spaces and small width
* causes content to wrap 1 word per line:
* https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
*/
.visually-hidden {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
/* 1 */
}
/*
* Extends the .visually-hidden class to allow the element
* to be focusable when navigated to via the keyboard:
* https://www.drupal.org/node/897638
*/
.visually-hidden.focusable:active,
.visually-hidden.focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
white-space: inherit;
width: auto;
}
/*
* Hide visually and from screen readers, but maintain layout
*/
.invisible {
visibility: hidden;
}
/*
* Clearfix: contain floats
*
* The use of `table` rather than `block` is only necessary if using
* `::before` to contain the top-margins of child elements.
*/
.clearfix::before,
.clearfix::after {
content: "";
display: table;
}
.clearfix::after {
clear: both;
}
/* ==========================================================================
EXAMPLE Media Queries for Responsive Design.
These examples override the primary ('mobile first') styles.
Modify as content requires.
========================================================================== */
@media only screen and (min-width: 35em) {
/* Style adjustments for viewports that meet the condition */
}
@media print,
(-webkit-min-device-pixel-ratio: 1.25),
(min-resolution: 1.25dppx),
(min-resolution: 120dpi) {
/* Style adjustments for high resolution devices */
}
/* ==========================================================================
Print styles.
Inlined to avoid the additional HTTP request:
https://www.phpied.com/delay-loading-your-print-css/
========================================================================== */
@media print {
*,
*::before,
*::after {
background: #fff !important;
color: #000 !important;
/* Black prints faster */
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]::after {
content: " (" attr(href) ")";
}
abbr[title]::after {
content: " (" attr(title) ")";
}
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^="#"]::after,
a[href^="javascript:"]::after {
content: "";
}
pre {
white-space: pre-wrap !important;
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
tr,
img {
page-break-inside: avoid;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}

0
img/.gitkeep Normal file
View file

51
index.html Normal file
View file

@ -0,0 +1,51 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" href="css/style.css">
<meta name="description" content="">
<meta property="og:title" content="">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<meta property="og:image:alt" content="">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon.png">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
</head>
<body>
<canvas id="canvas"></canvas>
<div id="console">
<label id="prompt">
<textarea id="command_input" class="multiline" autofocus>
start(0, 0);
go(1);
moving_pillars(5, 10,1, false);
turn(90);
moving_pillars(5, 10,1, false);
turn(90);
go(1);
moving_pillars(5, 10,1, false);
turn(90);
moving_pillars(5, 10,1, false);
</textarea>
</label>
</div>
<textarea id="help" style="visibility: hidden"></textarea>
<script src="js/app.js"></script>
</body>
</html>

121
js/app.js Normal file
View file

@ -0,0 +1,121 @@
"use strict";
import {interpret} from "./interpreter.js";
(function () {
const command_input_element = document.getElementById('command_input');
const canvas_element = document.getElementById('canvas');
const canvas = canvas_element.getContext('2d');
const width = canvas_element.width = window.innerWidth;
const height = canvas_element.height = window.innerHeight;
canvas.fillStyle = 'white';
canvas.strokeStyle = 'black';
canvas.lineWidth = 2;
const help_element = document.getElementById('help');
let script;
document.body.onkeydown = function (event) {
if (event.key === 'Tab') {
event.preventDefault();
}
}
const help = [
{v1: "start(x,y)", v2: "start(0,0);"},
{v1: "go(distance)", v2: "go(1);"},
{v1: "turn(angle)", v2: "turn(90);"},
{v1: "left(angle)", v2: "left(90);"},
{v1: "right(angle)", v2: "right(90);"},
{v1: "pillars(number, length, shift = 0, direction = UP)", v2: "pillars(3, 10, 0, DOWN);"},
{v1: "moving_pillars(number, length, shift = 0, direction = DOWN)", v2: "moving_pillars(3, 10, 0, DOWN);"},
{v1: "staircase(number, size, direction = DOWN)", v2: "staircase(3, 1, DOWN);"},
];
const slices = {};
for (let i = 0; i < help.length; i++) {
for (let j = 1; j < help[i].v1.length; j++) {
const sub = help[i].v1.substring(0, j);
if (!slices[sub]) {
slices[sub] = [];
}
slices[sub].push(help[i]);
}
slices[help[i].v1] = [help[i]];
}
let tx = 0;
let ty = 0;
let tangle = 0;
document.onkeyup = function (event) {
if (event.key === 'Tab') {
const script = command_input_element.value;
event.preventDefault();
const cursor_position = command_input_element.selectionStart;
const textUpToCursor = script.slice(0, cursor_position);
const startOfLine = textUpToCursor.lastIndexOf('\n') + 1;
const endOfLine = script.indexOf('\n', cursor_position);
const lineEndPosition = endOfLine === -1 ? script.length : endOfLine;
const currentLineText = script.slice(startOfLine, lineEndPosition).trim();
if (slices[currentLineText]) {
const helpOptions = slices[currentLineText].map(x => x.v2);
const firstOption = helpOptions[0];
command_input_element.value = script.slice(0, startOfLine) + firstOption + script.slice(lineEndPosition);
command_input_element.setSelectionRange(startOfLine + firstOption.length, startOfLine + firstOption.length);
command_input_element.focus();
help_element.style.visibility = 'hidden';
refresh();
}
}
};
function refresh() {
script = command_input_element.value;
try {
canvas.clearRect(0, 0, width, height);
interpret({canvas: canvas, x: tx, y: ty, angle: tangle, unit: 10, width, height}, script);
} catch (Exception) {
}
}
command_input_element.onkeyup = function handle_key_input(event) {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
script = command_input_element.value;
help_element.style.visibility = 'hidden';
const cursor_position = command_input_element.selectionStart;
const textUpToCursor = script.slice(0, cursor_position);
const startOfLine = textUpToCursor.lastIndexOf('\n') + 1;
const endOfLine = script.indexOf('\n', cursor_position);
const lineEndPosition = endOfLine === -1 ? script.length : endOfLine;
const currentLineText = script.slice(startOfLine, lineEndPosition);
if (currentLineText.length > 0) {
const command = currentLineText.trim();
if (slices[command]) {
const help = slices[command].map(x => x.v1);
const help_text = help.join('\n');
const computedStyle = window.getComputedStyle(command_input_element);
const fontSize = parseFloat(computedStyle.fontSize);
const lineHeight = computedStyle.lineHeight === 'normal' ? fontSize * 1.2 : parseFloat(computedStyle.lineHeight);
const textBounding = command_input_element.getBoundingClientRect();
const topPosition = textBounding.top + window.scrollY;// + (cursor_position - startOfLine) * lineHeight/ script.split('\n').length;
help_element.style.visibility = 'visible';
help_element.style.left = `${textBounding.left + window.scrollX}px`;
help_element.style.top = `${topPosition - help_element.offsetHeight}px`;
help_element.innerHTML = help_text;
help_element.style.display = 'block';
}
}
refresh();
}
};
refresh();
})();

241
js/interpreter.js Normal file
View file

@ -0,0 +1,241 @@
import {parse} from "./parser";
import {
BANG,
BANG_EQUAL,
EQUAL_EQUAL,
GREATER,
GREATER_EQUAL,
LESS,
LESS_EQUAL,
MINUS,
PLUS,
SLASH,
STAR
} from "./scanner";
export function interpret(init_env, code) {
let {canvas, tx, ty, tangle, unit, width, height} = init_env;
const UP = "UP";
const DOWN = "DOWN";
const LEFT = "LEFT";
const RIGHT = "RIGHT";
const BOTH = "BOTH";
Object.assign(init_env, {UP, DOWN, LEFT, RIGHT, BOTH});
const x0 = width / 2;
const y0 = height / 4;
let THIS = {
current_environment: init_env,
execute: (statement) => {
statement.accept(THIS);
},
visitBlockStatement: (statements) => {
THIS.executeBlock(statements, {enclosing: THIS.current_environment});
},
visitExpressionStatement: (expression) => {
THIS.evaluate(expression);
},
visitVariableStatement: (name, initializer) => {
let value = undefined;
if (initializer) {
value = THIS.evaluate(initializer);
}
THIS.current_environment[name.lexeme] = value;
},
visitPrintStatement: (expression) => {
console.log(THIS.current_environment);
let value = THIS.evaluate(expression);
console.log(THIS.stringify(value));
},
visitCallStatement: (fun, argList) => {
let args = argList.map(THIS.evaluate);
switch (fun){
case "start": THIS.start(...args); break;
case "go": THIS.go(...args); break;
case "turn": THIS.turn(...args); break;
case "left": THIS.left(...args); break;
case "right": THIS.right(...args); break;
case "pillars": THIS.pillars(...args); break;
case "moving_pillars": THIS.moving_pillars(...args); break;
case "staircase": THIS.staircase(...args); break;
default: throw "Unknown function: " + fun;
}
},
start: (x,y) => {
tx = x * unit;
ty = y * unit;
tangle = 0;
canvas.beginPath();
canvas.moveTo(x0 + tx, y0 + ty);
},
go: (distance) => {
tx += distance * unit * Math.cos(tangle);
ty += distance * unit * Math.sin(tangle);
canvas.lineTo(x0 + tx, y0 + ty);
canvas.stroke();
},
turn: (degrees) => {
tangle += degrees * Math.PI / 180;
},
left: (degrees) => {
THIS.turn(-degrees);
},
right: (degrees) => {
THIS.turn(degrees);
},
pillars: (number, length, shift = 0, direction = UP) => {
for (let i = 0; i < number; i++) {
THIS.turn(-90);
THIS.go(direction === DOWN ? -length : length);
THIS.turn(90);
THIS.go(1);
THIS.turn(90);
if (direction === BOTH) {
length += shift;
}
THIS.go(direction === DOWN ? -length : length);
THIS.turn(-90);
THIS.go(1);
length += shift;
}
},
moving_pillars: (n, length, shift = 0, direction = DOWN) => {
let length2 = length;
length += shift;
for (let i = 0; i < n; i++) {
THIS.turn(-90);
THIS.go(direction === DOWN ? -length2 : length2);
THIS.turn(90);
THIS.go(1);
THIS.turn(90);
THIS.go(direction === DOWN ? -length : length);
THIS.turn(-90);
THIS.go(1);
}
},
staircase: (number_of_steps, size, direction = DOWN) =>{
const angle = direction === DOWN ? 90 : -90;
for (let i = 0; i < number_of_steps; i++) {
THIS.go(size);
THIS.turn(angle);
THIS.go(size);
THIS.turn(-angle);
}
},
executeBlock: (statements, environment) => {
let previous = THIS.current_environment;
try {
THIS.current_environment = environment;
for (let i = 0; i < statements.length; i++) {
THIS.execute(statements[i]);
}
} finally {
THIS.current_environment = previous;
}
},
visitBinaryExpr: (operator, _left, _right) => {
let left = THIS.evaluate(_left);
let right = THIS.evaluate(_right);
switch (operator.type) {
case MINUS:
return left - right;
case SLASH:
return left / right;
case STAR:
return left * right;
case PLUS:
if (typeof left === 'number' && typeof right === 'number') {
return left + right;
} else {
return THIS.stringify(left) + THIS.stringify(right);
}
case GREATER:
return left > right;
case GREATER_EQUAL:
return left >= right;
case LESS:
return left < right;
case LESS_EQUAL:
return left <= right;
case BANG_EQUAL:
return left !== right;
case EQUAL_EQUAL:
return left === right;
}
throw "?";
},
visitGroupingExpr: (expr) => {
return THIS.evaluate(expr);
},
visitLiteralExpr: (value) => {
return value;
},
visitUnaryExpr: (operator, right_expr) => {
let right = THIS.evaluate(right_expr);
switch (operator.type) {
case MINUS:
return -right;
case BANG:
return !right;
default:
return undefined;
}
},
visitVariableExpr: (name) => {
return THIS.current_environment[name.lexeme];
},
visitAssignExpr: (name, value_expr) => {
let value = THIS.evaluate(value_expr);
THIS.current_environment[name] = value;
return value;
},
evaluate: (expr) => {
return expr.accept(THIS);
},
stringify: (value) => {
if (value === undefined || value === null) {
return "nil";
}
return value.toString();
}
};
try {
const statements = parse(code);
for (let i = 0; i < statements.length; i++) {
THIS.execute(statements[i]);
}
} catch (e) {
console.log(e);
}
}

258
js/parser.js Normal file
View file

@ -0,0 +1,258 @@
import "./scanner";
import {
scan, error,
BANG, BANG_EQUAL, EOF,
EQUAL, EQUAL_EQUAL,
FALSE, GREATER,
GREATER_EQUAL, IDENTIFIER,
LEFT_BRACE, LEFT_PAREN, LESS,
LESS_EQUAL, MINUS, NUMBER, PLUS,
PRINT, RIGHT_BRACE, RIGHT_PAREN,
SEMICOLON, SLASH, STAR, STRING, TRUE, VAR, START, COMMA, GO, STAIRCASE, PILLARS, MOVING_PILLARS, LEFT, RIGHT, TURN
} from "./scanner";
export function parse(code) {
const tokens = scan(code);
// console.log(tokens);
let current = 0;
const parse = () => {
let statements = [];
while (!is_at_end()) {
statements.push(declaration());
}
return statements;
}
const declaration = () => {
if (match(VAR)) {
return varDeclaration();
}
return statement();
}
const varDeclaration = () => {
const name = consume(IDENTIFIER, "Expected a variable name");
let initializer;
if (match(EQUAL)) {
initializer = expression();
}
consume(SEMICOLON, "Expected semicolon");
return {accept: (visitor) => visitor.visitVariableStatement(name, initializer)};
}
const statement = () => {
if (match(PRINT)) {
return printStatement();
}
if (match(START)) {
return callStatement("start");
}
if (match(GO)) {
return callStatement("go");
}
if (match(LEFT)) {
return callStatement("left");
}
if (match(RIGHT)) {
return callStatement("right");
}
if (match(TURN)) {
return callStatement("turn");
}
if (match(STAIRCASE)) {
return callStatement("staircase");
}
if (match(PILLARS)) {
return callStatement("pillars");
}
if (match(MOVING_PILLARS)) {
return callStatement("moving_pillars");
}
if (match(LEFT_BRACE)) {
return {accept: (visitor) => visitor.visitBlockStatement(block())};
}
return expressionStatement();
}
const expression = () => {
return assignment();
}
const expressions = () => {
consume(LEFT_PAREN, "Expect '('");
let exprs = [expression()];
while (match(COMMA)) {
exprs.push(expression());
}
consume(RIGHT_PAREN, "Expect ')'");
return exprs;
}
const assignment = () => {
let expr = equality();
if (match(EQUAL)) {
let equals = previous();
let value = assignment();
if (expr.class === 'Variable') {
let name = expr.name;
return {accept: (visitor) => visitor.visitAssignExpr(name, value)};
}
throw error(equals, "Invalid assignment target.");
}
return expr;
}
const printStatement = () => {
const value = expression();
consume(SEMICOLON, "Expected semicolon");
return {accept: (visitor) => visitor.visitPrintStatement(value)};
}
const callStatement = (name) => {
const values = expressions();
consume(SEMICOLON, "Expected semicolon");
return {accept: (visitor) => visitor.visitCallStatement(name, values)};
}
const expressionStatement = () => {
const value = expression();
consume(SEMICOLON, "Expected semicolon");
return {accept: (visitor) => visitor.visitExpressionStatement(value)};
}
const block = () => {
let statements = [];
while (!check(RIGHT_BRACE) && !is_at_end()) {
statements.push(declaration());
}
consume(RIGHT_BRACE, "Expect '}' after block.");
return statements;
}
const equality = () => {
let expr = comparison();
while (match(BANG_EQUAL, EQUAL_EQUAL)) {
let operator = previous();
let right = comparison();
expr = {accept: (visitor) => visitor.visitBinaryExpr(operator, expr, right)};
}
return expr;
}
const comparison = () => {
let expr = term();
while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) {
let operator = previous();
let right = term();
expr = {accept: (visitor) => visitor.visitBinaryExpr(operator, expr, right)};
}
return expr;
}
const term = () => {
let expr = factor();
while (match(MINUS, PLUS)) {
let operator = previous();
let right = factor();
expr = {accept: (visitor) => visitor.visitBinaryExpr(operator, expr, right)};
}
return expr;
}
const factor = () => {
let expr = unary();
while (match(SLASH, STAR)) {
let operator = previous();
let right = unary();
expr = {accept: (visitor) => visitor.visitBinaryExpr(operator, expr, right)};
}
return expr;
}
const unary = () => {
if (match(BANG, MINUS)) {
let operator = previous();
let right = unary();
return {accept: (visitor) => visitor.visitUnaryExpr(operator, right)};
}
return primary();
}
const primary = () => {
if (match(FALSE)) {
return {accept: (visitor) => visitor.visitLiteralExpr(false)};
}
if (match(TRUE)) {
return {accept: (visitor) => visitor.visitLiteralExpr(true)};
}
if (check(NUMBER)) {
advance();
let number = parseFloat(previous().literal);
return {accept: (visitor) => visitor.visitLiteralExpr(number)};
}
if (check(STRING)) {
advance();
let string = previous().literal;
return {accept: (visitor) => visitor.visitLiteralExpr(string)};
}
if (match(IDENTIFIER)) {
let identifier = previous();
return {class: "Variable", accept: (visitor) => visitor.visitVariableExpr(identifier)};
}
if (match(LEFT_PAREN)) {
let expr = expression();
consume(RIGHT_PAREN, "Expect ')' after expression.");
return {accept: (visitor) => visitor.visitGroupingExpr(expr)};
}
throw error(peek(), "Expect expression.");
}
const consume = (type, message) => {
if (check(type)) {
return advance();
}
throw error(peek(), message);
}
const match = (...types) => {
for (let i = 0; i < types.length; i++) {
if (check(types[i])) {
advance();
return true;
}
}
return false;
}
const check = (type) => {
if (is_at_end()) {
return false;
}
return peek().type === type;
}
const advance = () => {
if (!is_at_end()) {
current++;
}
return previous();
}
const is_at_end = () => {
return peek().type === EOF;
}
const peek = () => {
return tokens[current];
}
const previous = () => {
return tokens[current - 1];
}
return parse();
}

266
js/scanner.js Normal file
View file

@ -0,0 +1,266 @@
export const scan = (source) => {
const tokens = [];
let start = 0;
let current = 0;
let line = 1;
const scan_tokens = () => {
while (!is_at_end()) {
start = current;
scan_token();
}
tokens.push({type: EOF, lexeme: "", line: line});
return tokens;
};
const is_at_end = () => {
return current >= source.length;
}
const advance = () => {
return source.charAt(current++);
}
const add_token = (type, literal) => {
tokens.push({type: type, lexeme: source.substring(start, current), literal: literal, line: line});
}
const scan_token = () => {
let c = advance();
switch (c) {
case '(':
add_token(LEFT_PAREN);
break;
case ')':
add_token(RIGHT_PAREN);
break;
case '{':
add_token(LEFT_BRACE);
break;
case '}':
add_token(RIGHT_BRACE);
break;
case ',':
add_token(COMMA);
break;
case '.':
add_token(DOT);
break;
case '-':
add_token(MINUS);
break;
case '+':
add_token(PLUS);
break;
case ';':
add_token(SEMICOLON);
break;
case '*':
add_token(STAR);
break;
case '!':
add_token(match('=') ? BANG_EQUAL : BANG);
break;
case '=':
add_token(match('=') ? EQUAL_EQUAL : EQUAL);
break;
case '<':
add_token(match('=') ? LESS_EQUAL : LESS);
break;
case '>':
add_token(match('=') ? GREATER_EQUAL : GREATER);
break;
case '/':
if (match('/')) {
while (peek() !== '\n' && !is_at_end()) {
advance();
}
} else {
add_token(SLASH);
}
break;
case ' ':
case '\r':
case '\t':
break;
case '\n':
line++;
break;
case '"':
string();
break;
default:
if (is_digit(c)) {
number();
} else if (is_alpha(c)) {
identifier();
} else {
throw error(line, "Unexpected character");
}
}
}
const identifier = () => {
while (is_alphaNumeric(peek())) {
advance();
}
let text = source.substring(start, current);
let type = keywords.get(text);
if (type == null) {
type = IDENTIFIER;
}
add_token(type);
}
const is_alphaNumeric = (c) => {
return is_alpha(c) || is_digit(c);
}
const is_alpha = (c) => {
return typeof c === 'string' && (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_';
}
const number = () => {
while (is_digit(peek())) {
advance();
}
if (peek() === '.' && is_digit(peekNext())) {
advance();
}
while (is_digit(peek())) {
advance();
}
add_token(NUMBER, source.substring(start, current));
}
const string = () => {
while (peek() !== '"' && !is_at_end()) {
if (peek() === '\n') {
line++;
}
advance();
}
if (is_at_end()) {
throw new Error(error(line, "Unterminated string"));
}
advance();
let value = source.substring(start + 1, current - 1);
console.log("string "+value);
add_token(STRING, value);
}
const peekNext = () => {
return source.charAt(current + 1);
}
const is_digit = (c) => {
return typeof c === 'string' && c >= '0' && c <= '9';
}
const peek = () => {
if (is_at_end()) {
return '\0';
}
return source.charAt(current);
}
const match = (expected) => {
if (is_at_end()) {
return false;
}
if (source.charAt(current) !== expected) {
return false;
}
current++;
return true;
}
return scan_tokens();
}
export const error = (token, message) => {
if (token.type === EOF) {
return report(token.line, " at end", message);
} else {
return report(token.line, "", message);
}
}
export const report = (line, where, message) => {
return ("[line " + line + "] Error" + where + ": " + message);
}
export const LEFT_PAREN = 1;
export const RIGHT_PAREN = 2;
export const LEFT_BRACE = 3;
export const RIGHT_BRACE = 4;
export const COMMA = 5;
export const DOT = 6;
export const MINUS = 7;
export const PLUS = 8;
export const SEMICOLON = 9;
export const SLASH = 10;
export const STAR = 11;
export const BANG = 12;
export const BANG_EQUAL = 13;
export const EQUAL = 14;
export const EQUAL_EQUAL = 15;
export const GREATER = 16;
export const GREATER_EQUAL = 17;
export const LESS = 18;
export const LESS_EQUAL = 19;
export const IDENTIFIER = 20;
export const STRING = 21;
export const NUMBER = 22;
export const AND = 23;
export const ELSE = 24;
export const FALSE = 25;
export const FUN = 26;
export const FOR = 27;
export const IF = 28;
export const OR = 30;
export const PRINT = 31;
export const RETURN = 32;
export const TRUE = 33;
export const VAR = 34;
export const WHILE = 35;
export const EOF = 36;
export const START = 37;
export const GO = 38;
export const TURN = 39;
export const LEFT = 40;
export const RIGHT = 41;
export const PILLARS = 42;
export const MOVING_PILLARS = 43;
export const STAIRCASE = 44;
export const keywords = new Map([
["and", AND],
["else", ELSE],
["false", FALSE],
["fun", FUN],
["for", FOR],
["if", IF],
["or", OR],
["print", PRINT],
["return", RETURN],
["true", TRUE],
["var", VAR],
["while", WHILE],
["start", START],
["go", GO],
["turn", TURN],
["left", LEFT],
["right", RIGHT],
["pillars", PILLARS],
["moving_pillars", MOVING_PILLARS],
["staircase", STAIRCASE]
])

0
js/vendor/.gitkeep vendored Normal file
View file

4494
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": " ",
"version": "0.0.1",
"description": "",
"private": true,
"keywords": [
""
],
"license": "",
"author": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack serve --open --config webpack.config.dev.js",
"build": "webpack --config webpack.config.prod.js"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
}
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
szpakowski.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

26
szpaks/radiator.szpak Normal file
View file

@ -0,0 +1,26 @@
start(-54,20);
go(1);
for (let i=0; i<4; i++){
pillars(13, 2, 1, UP);
}
left(90);
go(18);
left(90);
go(1)
for (let i=0; i<4; i++){
pillars(13, 2, 1, UP);
}
left(90);
go(37);
left(90);
go(1)
for (let i=0; i<4; i++){
pillars(13, 2, 1, UP);
}
left(90);
go(18);
left(90);
go(1)
for (let i=0; i<4; i++){
pillars(13, 2, 1, UP);
}

12
webpack.common.js Normal file
View file

@ -0,0 +1,12 @@
const path = require('path');
module.exports = {
entry: {
app: './js/app.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
clean: true,
filename: './js/app.js',
},
};

13
webpack.config.dev.js Normal file
View file

@ -0,0 +1,13 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
liveReload: true,
hot: true,
open: true,
static: ['./'],
},
});

25
webpack.config.prod.js Normal file
View file

@ -0,0 +1,25 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
new CopyPlugin({
patterns: [
{ from: 'img', to: 'img' },
{ from: 'css', to: 'css' },
{ from: 'js/vendor', to: 'js/vendor' },
{ from: 'icon.svg', to: 'icon.svg' },
{ from: 'favicon.ico', to: 'favicon.ico' },
{ from: 'robots.txt', to: 'robots.txt' },
{ from: 'icon.png', to: 'icon.png' },
{ from: 'site.webmanifest', to: 'site.webmanifest' },
],
}),
],
});