Asked by Alex

Can you make me a small interpreter for a small programming language written in html

All Answers 1

Answered by GPT-5 mini AI
Below is a single-file HTML + JavaScript "toy interpreter" you can open in a browser. It implements a tiny imperative language with:
- let NAME = EXPR (assignment)
- print EXPR (print value / string)
- if EXPR then ... end (conditional blocks, nestable)
- while EXPR do ... end (loops, nestable)
- comments with # at line start
- expressions: numbers, quoted strings, identifiers, + - * / % unary !, comparisons == != < <= > >=, logical && ||, parentheses

Copy the whole file into a file like tiny-interpreter.html and open it in a browser.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Tiny Interpreter</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 16px; }
textarea { width: 100%; height: 260px; font-family: monospace; font-size: 13px; }
pre { background: #111; color: #0f0; padding: 10px; min-height: 120px; white-space: pre-wrap; }
.controls { margin: 8px 0 16px 0; }
button { margin-right: 8px; }
label { font-weight: bold; display:block; margin-top:8px; }
</style>
</head>
<body>
<h2>Tiny Interpreter</h2>
<label>Program</label>
<textarea id="program"></textarea>
<div class="controls">
<button onclick="runProgram()">Run</button>
<button onclick="clearOutput()">Clear Output</button>
<button onclick="loadExample()">Load Example</button>
</div>
<label>Output</label>
<pre id="output"></pre>

<script>
// Tiny language interpreter

// Utility: show output
function appendOutput(text) {
const out = document.getElementById('output');
out.textContent += text + "\\n";
}
function clearOutput() {
document.getElementById('output').textContent = '';
}

// Tokenizer for expressions
function tokenizeExpr(s) {
const tokens = [];
let i = 0;
while (i < s.length) {
const ch = s[i];
if (/\s/.test(ch)) { i++; continue; }
// number
if (/\d/.test(ch) || (ch === '.' && /\d/.test(s[i+1]))) {
let j = i;
while (j < s.length && /[\d.]/.test(s[j])) j++;
tokens.push({type:'number', value: parseFloat(s.slice(i,j))});
i = j;
continue;
}
// identifier
if (/[A-Za-z_]/.test(ch)) {
let j = i;
while (j < s.length && /[A-Za-z0-9_]/.test(s[j])) j++;
tokens.push({type:'ident', value: s.slice(i,j)});
i = j;
continue;
}
// string literal (double quoted)
if (ch === '"') {
let j = i+1;
let val = '';
while (j < s.length) {
if (s[j] === '"' && s[j-1] !== '\\') break;
if (s[j] === '\\' && j+1 < s.length) {
const esc = s[j+1];
if (esc === 'n') val += '\\n';
else if (esc === 't') val += '\\t';
else val += esc;
j += 2;
continue;
}
val += s[j++];
}
if (j >= s.length) throw new Error("Unterminated string");
tokens.push({type:'string', value: val});
i = j+1;
continue;
}
// multi-char operators
const two = s.slice(i,i+2);
if (two === '==' || two === '!=' || two === '<=' || two === '>=' || two === '&&' || two === '||') {
tokens.push({type:'op', value: two});
i += 2;
continue;
}
// single char operators and parentheses
if ('+-*/%()!<>'.includes(ch)) {
tokens.push({type:'op', value: ch});
i++;
continue;
}
throw new Error('Unexpected char in expression: ' + ch);
}
return tokens;
}

// Shunting-yard: convert infix tokens to RPN
function infixToRPN(tokens) {
const out = [];
const ops = [];
const precedence = {
'!' : 7,
'*' : 6, '/' : 6, '%' : 6,
'+' : 5, '-' : 5,
'<' : 4, '<=':4, '>' :4, '>=':4,
'==':3, '!=':3,
'&&':2,
'||':1
};
const rightAssoc = { '!': true };
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
if (t.type === 'number' || t.type === 'string' || t.type === 'ident') {
out.push(t);
} else if (t.type === 'op') {
if (t.value === '(') {
ops.push(t.value);
} else if (t.value === ')') {
while (ops.length && ops[ops.length-1] !== '(') out.push({type:'op', value: ops.pop()});
if (!ops.length) throw new Error("Mismatched parentheses");
ops.pop(); // pop '('
} else {
const curPrec = precedence[t.value];
if (curPrec === undefined) throw new Error("Unknown operator: " + t.value);
while (ops.length) {
const top = ops[ops.length-1];
if (top === '(') break;
const topPrec = precedence[top];
if ( (rightAssoc[t.value] && curPrec < topPrec) || (!rightAssoc[t.value] && curPrec <= topPrec) ) {
out.push({type:'op', value: ops.pop()});
} else break;
}
ops.push(t.value);
}
}
}
while (ops.length) {
const v = ops.pop();
if (v === '(' || v === ')') throw new Error("Mismatched parentheses");
out.push({type:'op', value: v});
}
return out;
}

// Evaluate RPN with environment (vars)
function evalRPN(rpn, env) {
const stack = [];
for (let t of rpn) {
if (t.type === 'number' || t.type === 'string') {
stack.push(t.value);
} else if (t.type === 'ident') {
if (!(t.value in env)) stack.push(undefined);
else stack.push(env[t.value]);
} else if (t.type === 'op') {
const op = t.value;
if (op === '!') {
const a = stack.pop();
stack.push(!a);
continue;
}
const b = stack.pop();
const a = stack.pop();
switch(op) {
case '+':
if (typeof a === 'string' || typeof b === 'string') stack.push(String(a) + String(b));
else stack.push(a + b);
break;
case '-': stack.push(a - b); break;
case '*': stack.push(a * b); break;
case '/': stack.push(a / b); break;
case '%': stack.push(a % b); break;
case '==': stack.push(a == b); break;
case '!=': stack.push(a != b); break;
case '<': stack.push(a < b); break;
case '<=': stack.push(a <= b); break;
case '>': stack.push(a > b); break;
case '>=': stack.push(a >= b); break;
case '&&': stack.push(a && b); break;
case '||': stack.push(a || b); break;
default: throw new Error('Unknown operator during eval: ' + op);
}
} else {
throw new Error('Unknown RPN token: ' + JSON.stringify(t));
}
}
if (stack.length !== 1) {
// allow undefined as a result sometimes
return stack[stack.length-1];
}
return stack[0];
}

// Parse program into AST (statements)
function parseProgram(source) {
// split into lines, but we need to preserve nested blocks; we'll parse with a pointer
const rawLines = source.split('\\n');
// Preprocess: trim trailing spaces; treat full-line comments starting with #
const lines = rawLines.map(l => l.replace(/\\r/g,'')).map(l => l.trim()).filter(l=>l.length>0 && !l.startsWith('#'));
let i = 0;
function parseBlock() {
const stmts = [];
while (i < lines.length) {
const line = lines[i];
// handle end markers
if (line === 'end' || line === 'else') break; // end of current block
// while ... do
const mWhile = line.match(/^while\\s+(.+)\\s+do$/);
if (mWhile) {
i++;
const condSrc = mWhile[1];
const condRPN = infixToRPN(tokenizeExpr(condSrc));
const body = parseBlock();
// expect end
if (i >= lines.length || lines[i] !== 'end') throw new Error("Expected 'end' for while starting at line " + (i+1));
i++; // consume end
stmts.push({type:'while', cond: condRPN, body});
continue;
}
// if ... then
const mIf = line.match(/^if\\s+(.+)\\s+then$/);
if (mIf) {
i++;
const condSrc = mIf[1];
const condRPN = infixToRPN(tokenizeExpr(condSrc));
const body = parseBlock();
if (i >= lines.length || lines[i] !== 'end') throw new Error("Expected 'end' for if starting at line " + (i+1));
i++; // consume end
stmts.push({type:'if', cond: condRPN, body});
continue;
}
// print
const mPrint = line.match(/^print\\s+(.+)$/);
if (mPrint) {
const expr = mPrint[1];
const rpn = infixToRPN(tokenizeExpr(expr));
stmts.push({type:'print', expr: rpn});
i++; continue;
}
// let assignment: let x = expr
const mLet = line.match(/^let\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.+)$/);
if (mLet) {
const name = mLet[1];
const expr = mLet[2];
const rpn = infixToRPN(tokenizeExpr(expr));
stmts.push({type:'let', name, expr: rpn});
i++; continue;
}
// assignment without let: x = expr
const mAssign = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.+)$/);
if (mAssign) {
const name = mAssign[1];
const expr = mAssign[2];
const rpn = infixToRPN(tokenizeExpr(expr));
stmts.push({type:'assign', name, expr: rpn});
i++; continue;
}
throw new Error("Unknown statement: " + line);
}
return stmts;
}
i = 0;
const ast = parseBlock();
if (i < lines.length) {
// leftover 'end' or similar
// ignore
}
return ast;
}

// Execute AST
function execute(ast, env) {
for (const s of ast) {
if (s.type === 'print') {
const v = evalRPN(s.expr, env);
appendOutput(String(v));
} else if (s.type === 'let' || s.type === 'assign') {
const v = evalRPN(s.expr, env);
env[s.name] = v;
} else if (s.type === 'if') {
const cond = evalRPN(s.cond, env);
if (cond) {
execute(s.body, env);
}
} else if (s.type === 'while') {
let safety = 100000; // avoid infinite loop in accidental user code
while (evalRPN(s.cond, env)) {
execute(s.body, env);
if (--safety <= 0) throw new Error("Loop exceeded step limit");
}
} else {
throw new Error('Unknown statement type: ' + s.type);
}
}
}

// Run the program from textarea
function runProgram() {
clearOutput();
const src = document.getElementById('program').value;
try {
const ast = parseProgram(src);
const env = {}; // variables
execute(ast, env);
} catch (e) {
appendOutput('Error: ' + e.message);
console.error(e);
}
}

// Example program
function loadExample() {
const sample = [
'# Example: factorial and loop',
'let n = 7',
'let fact = 1',
'while n > 1 do',
' fact = fact * n',
' n = n - 1',
'end',
'print "factorial = " + fact',
'',
'# count down',
'let i = 5',
'while i > 0 do',
' print "i = " + i',
' i = i - 1',
'end',
'',
'# conditional',
'let x = 10',
'if x > 5 then',
' print "x is large"',
'end'
].join('\\n');
document.getElementById('program').value = sample;
clearOutput();
}

// load example on open
loadExample();
</script>
</body>
</html>

Notes:
- It's intentionally simple and not production-grade. It parses line-based statements and nested blocks. Expressions are evaluated without using eval, using a tokenizer + shunting-yard + RPN evaluator.
- You can extend it: add functions, arrays, string escaping improvements, break/continue, or more operators.
- If you want I can adapt it (smaller/ simpler, or add features like functions, arrays, or a nicer editor UI).