UNPKG

yip-core

Version:

basic acces tp the yip (yet-interpreted-programming) c & dr models (Yip compiler & direct-runner)

824 lines (714 loc) 20.5 kB
// yip.js class YipError extends Error { constructor(message, code, tokenIndex, source, tokens) { super(message); this.name = 'YipError'; this.code = code; this.pos = tokenIndex; const { line, col } = YipError.findLineAndCol(tokenIndex, source, tokens); this.line = line; this.col = col; } static findLineAndCol(index, source, tokens) { let line = 1; let col = 1; let i = 0; let tokenCount = 0; while (i < source.length && tokenCount <= index) { // Skip whitespace while (/\s/.test(source[i])) { if (source[i] === '\n') { line++; col = 1; } else { col++; } i++; } const token = tokens[tokenCount]; if (!token) break; if (source.slice(i, i + token.length) === token) { if (tokenCount === index) { return { line, col }; } i += token.length; col += token.length; tokenCount++; } else { // Mismatch: consume 1 char (bad source), continue i++; col++; } } return { line, col }; } get location() { return `line ${this.line}, col ${this.col}`; } toString() { return `${this.name} [${this.code}]: ${this.message} at ${this.location}`; } } class ASTNode { constructor(type) { this.type = type; } } class YipTokenizer { constructor(grammar = '<str|keyword|ident|num|sym>', options = {}) { this.grammar = grammar; this.mode = options.mode || 'tokens'; this.keywords = new Set(options.keywords || []); this.rules = this.buildRules(grammar); } buildRules(grammar) { const rules = []; const parts = grammar.replace(/[<>]/g, '').split('|'); for (const part of parts) { switch (part.trim()) { case 'str': rules.push({ type: 'str', regex: /^"([^"\\]*(\\.[^"\\]*)*)"/ }); rules.push({ type: 'str', regex: /^'([^'\\]*(\\.[^'\\]*)*)'/ }); break; case 'num': rules.push({ type: 'num', regex: /^\d+(\.\d+)?/ }); break; case 'ident': rules.push({ type: 'ident', regex: /^[a-zA-Z_$][a-zA-Z0-9_$]*/, resolve: (v, tokenizer) => tokenizer.keywords.has(v) ? { type: 'keyword', value: v } : { type: 'ident', value: v }, }); break; case 'keyword': rules.push({ type: 'keyword', regex: /^[a-zA-Z_][a-zA-Z0-9_]*/, resolve: (v, tokenizer) => tokenizer.keywords.has(v) ? { type: 'keyword', value: v } : null, }); break; case 'sym': rules.push({ type: 'sym', regex: /^[\[\]{}()!;,+\-*/=<>]/ }); break; } } return rules; } tokenize(input) { const tokens = []; let i = 0; let line = 1; let col = 1; while (i < input.length) { if (/\s/.test(input[i])) { if (input[i] === '\n') { line++; col = 1; } else { col++; } i++; continue; } let matched = false; for (const rule of this.rules) { const slice = input.slice(i); const match = slice.match(rule.regex); if (match) { matched = true; let token = { type: rule.type, value: match[0] }; if (rule.resolve) { const resolved = rule.resolve(match[0], this); if (resolved) token = resolved; else continue; } const tokenObj = this.mode === 'tokens' ? match[0] : { ...token, offset: i, line, col }; tokens.push(tokenObj); const len = match[0].length; for (let k = 0; k < len; k++) { if (input[i + k] === '\n') { line++; col = 1; } else { col++; } } i += len; break; } } if (!matched) { throw new SyntaxError(`Unrecognized token at: "${input.slice(i, i + 10)}..."`); } } return tokens; } } class ProgramNode extends ASTNode { constructor(body = []) { super('Program'); this.body = body; } } class CommandNode extends ASTNode { constructor(name, args = []) { super('Command'); this.name = name; this.args = args; } } class LiteralNode extends ASTNode { constructor(value) { super('Literal'); this.value = value; } } class Yip { static defineKit(name, fn) { Yip._kits = Yip._kits || {}; Yip._kits[name] = fn; } constructor(tokens, pos = 0, code = tokens.join(" ")) { this.tokens = tokens; this.commands = {}; this.compiledCommands = {}; this.byteCommands = {}; this._bytecodeHandlers = {}; this.commandMeta = {}; this.pos = pos; this.code = code; this.vars = {}; this.macros = {}; this.kits = { ...(Yip._kits || {}) }; // ✅ initialize from shared this._compiled = ''; this._bytecode = ''; } applyKit(name, config = {}) { const kitFn = this.kits[name]; if (!kitFn) throw new Error(`Unknown kit '${name}'`); return kitFn.call(this, config); } register(name, desc, example, action) { this.commands[name] = action; this.commandMeta[name] = { desc, example }; return this; } registerCompiler(name, desc, example, action) { this.compiledCommands[name] = action; this.commandMeta[name] = { desc, example }; return this; } semicolonEnding(handler) { while (true) { handler.call(this); const next = this.peek(); if (next === ';') { this.expect(';'); break; } else if (next === ',') { this.expect(','); continue; } else { throw new YipError( `Expected ';' or ',' but got '${next}'`, 'YIP_ENDING', this.pos, this.code, this.tokens ); } } } registerByteCommand(name, desc, example, fn) { this.byteCommands[name] = fn; this.commandMeta[name] = { desc, example }; return this; } addBytecodeHandler(name, fn) { this._bytecodeHandlers[name] = fn; return this; } parseAST() { this.applyMacros(); const body = []; while (!this.isAtEnd()) { const tok = this.next(); if ( this.commands[tok] || this.compiledCommands[tok] || this.byteCommands[tok] ) { const node = new CommandNode(tok); const next = this.peek(); if (next === '!') { this.expect('!'); if (this.peek() === '(') { const args = this.parseParen().split(/\s+/).filter(Boolean); node.args = args.map((a) => new LiteralNode(a)); } } this.expect(';'); body.push(node); } else { console.warn(`⚠️ Unknown token in AST: '${tok}'`); } } return new ProgramNode(body); } astRun(ast) { this.applyMacros(); for (const node of ast.body) { if (node.type === 'Command') { const fn = this.commands[node.name]; if (fn) fn.call(this, ...node.args.map((a) => a.value)); } } } astCompile(ast) { let out = ''; this.applyMacros(); for (const node of ast.body) { if (node.type === 'Command') { const fn = this.compiledCommands[node.name]; if (fn) { out += fn.call(this, ...node.args.map((a) => a.value)) + '\n'; } } } return out.trim(); } execute() { if (!this.validate()) { console.warn("⚠️ Script validation failed. Execution aborted."); return; } try { this.applyMacros(); this.runBasedOnCommands(); } catch (err) { console.error(`❌ Runtime error: ${err.message || err}`); } return this; } command(name, fn) { this.commands[name] = fn; return this; } c_command(name, fn) { this.compiledCommands[name] = fn; return this; } describeCommand(name, desc, example) { this.commandMeta[name] = { desc, example }; return this; } macro(name, replacerFn) { this.macros[name] = replacerFn; return this; } applyMacros() { let i = 0; while (i < this.tokens.length) { const tok = this.tokens[i]; if (this.macros[tok]) { const before = this.tokens.slice(0, i); const after = this.tokens.slice(i + 1); const injected = this.macros[tok].call(this); this.tokens = [...before, ...injected, ...after]; i += injected.length; } else { i++; } } } compile() { this._compiled = ''; this.applyMacros(); while (!this.isAtEnd()) { const tok = this.next(); const handler = this.compiledCommands[tok]; if (handler) { const code = handler.call(this); this._compiled += code + '\n'; } else { console.warn(`Unknown compile command: ${tok}`); } } return this; } getCompiled() { return this._compiled.trim(); } toBytecode() { this._bytecode = ''; this.applyMacros(); while (!this.isAtEnd()) { const tok = this.next(); const fn = this.byteCommands[tok]; if (fn) { const encoded = fn.call(this); this._bytecode += encoded + '\n'; } else { console.warn(`Unknown byte command: ${tok}`); } } return this; } getBytecode() { return this._bytecode.trim(); } executeBytecode() { const lines = this._bytecode.trim().split('\n'); this.applyMacros(); for (const line of lines) { if (!line.trim()) continue; const [opcode, ...args] = line.trim().split(/\s+/); const handler = this._bytecodeHandlers[opcode]; if (!handler) { console.warn(`⚠️ Unknown bytecode opcode: ${opcode}`); continue; } try { handler(args); } catch (err) { console.error(`❌ Bytecode execution error: ${err.message}`); } } } runBasedOnCommands() { this.applyMacros(); while (!this.isAtEnd()) { const tok = this.next(); const cmd = this.commands[tok]; if (cmd) { cmd.call(this); } else { console.warn(`Unknown command: ${tok}`); } } } validate() { const known = new Set([ ...Object.keys(this.commands), ...Object.keys(this.compiledCommands), ...Object.keys(this.byteCommands), ...Object.keys(this.macros), ]); const stack = []; const openers = { '(': ')', '{': '}', '[': ']' }; const closers = new Set(Object.values(openers)); let success = true; for (let i = 0; i < this.tokens.length; i++) { const tok = this.tokens[i]; if (/^['"`]/.test(tok)) { const quote = tok[0]; if (!tok.endsWith(quote) || tok.length === 1) { console.warn(`⚠️ Unclosed quote at token ${i}: ${tok}`); success = false; } } if (openers[tok]) { stack.push({ char: tok, i }); } else if (closers.has(tok)) { const last = stack.pop(); if (!last || openers[last.char] !== tok) { console.warn(`⚠️ Unmatched closing '${tok}' at token ${i}`); success = false; } } if (!known.has(tok) && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(tok)) { const next = this.tokens[i + 1]; if (!known.has(next) && next !== '!') { console.warn(`⚠️ Unknown command or identifier: '${tok}'`); success = false; } } } if (stack.length > 0) { stack.forEach((s) => { console.warn(`⚠️ Unmatched opening '${s.char}' at token ${s.i}`); }); success = false; } return success; } helpMenu() { console.log(`\x1b[1m\x1b[34mAvailable Commands:\x1b[0m`); for (const cmd in this.commandMeta) { const { desc, example } = this.commandMeta[cmd]; console.log(`\x1b[33m${cmd}\x1b[0m — ${desc}`); if (example) { console.log(` \x1b[90mex:\x1b[0m ${example}`); } } } executionLoop(posit, fn) { while (!this.isAtEnd()) { const result = fn.call(this, this.tokens[posit]); if (result === 'break') break; } } expect = (expected) => { const actual = this.tokens[this.pos]; if (actual !== expected) { throw new YipError( `Expected '${expected}' but got '${actual ?? '<end of input>'}'`, 'YIP_EXPECT', this.pos, this.code, this.tokens ); } this.pos++; return this; }; parseDelimited = (start, end) => { this.expect(start); let body = '', count = 1; while (count > 0) { const tok = this.next(); if (tok === start) count++; else if (tok === end) count--; if (count === 0) break; if (this.pos > this.tokens.length) { throw new YipError(`Missing closing '${end}'`, 'YIP_DELIM', this.pos, this.code, this.tokens); } body += tok + ' '; } return body.trim(); }; parseParen = () => this.parseDelimited('(', ')'); parseBlock = () => this.parseDelimited('{', '}'); parseUntilSem = () => { let body = '', braceCount = 0; while (true) { const tok = this.next(); if (tok === '{' || tok === '[' || tok === '(') braceCount++; if (tok === '}' || tok === ']' || tok === ')') braceCount--; if (tok === ';' && braceCount === 0) break; if (this.pos > this.tokens.length) { throw new YipError(`Missing closing ';'`, 'YIP_SEMI', this.pos, this.code, this.tokens); } body += tok + ' '; } return body.trim(); }; parseUntil = (xf) => { let body = ''; while (true) { const tok = this.next(); if (tok === xf) break; if (this.pos > this.tokens.length) { throw new YipError(`Missing closing '${xf}'`, 'YIP_UNTIL', this.pos, this.code, this.tokens); } body += tok + ' '; } return body.trim(); }; next() { return this.tokens[this.pos++]; } peek() { return this.tokens[this.pos]; } back() { return this.tokens[--this.pos]; } eat() { const tok = this.tokens[this.pos]; this.pos++; return tok; } resolve(value) { if (value in this.vars) return this.vars[value]; if (value === 'true' || value === 'false') return value; return value.replace(/^"|"$/g, ''); } isAtEnd() { return this.pos >= this.tokens.length; } smart_command = function(signature, fn) { const parts = []; const tokenRegex = /<[^>]+>|\[[^\]]+\]|\S+/g; let match; while ((match = tokenRegex.exec(signature))) { const token = match[0]; if (token.startsWith('<') && token.endsWith('>')) { parts.push({ type: 'arg', name: token.slice(1, -1), spread: false }); } else if (token.startsWith('[') && token.endsWith(']')) { const name = token.slice(1, -1).replace(/^\.{3}/, ''); parts.push({ type: 'arg', name, spread: token.includes('...') }); } else { parts.push({ type: 'literal', value: token }); } } const name = parts[0].type === 'literal' ? parts[0].value : null; if (!name) throw new Error('smart_command must start with literal command name'); this.command(name, function () { const argsOut = {}; let partIndex = 0; while (partIndex < parts.length) { const part = parts[partIndex]; if (part.type === 'literal') { const got = this.next(); if (got !== part.value) { throw new YipError(`Expected literal '${part.value}' but got '${got}'`, 'YIP_LITERAL', this.pos, this.code, this.tokens); } partIndex++; } else if (part.type === 'arg') { if (part.spread) { const raw = this.parseParen(); // expect (...) list const items = raw.split(',').map(s => s.trim()).filter(Boolean); argsOut[part.name] = items; partIndex++; } else { const val = this.next(); argsOut[part.name] = val; partIndex++; } } } // flatten args by definition order const finalArgs = parts .filter(p => p.type === 'arg') .flatMap(p => (p.spread ? argsOut[p.name] : [argsOut[p.name]])); return fn.apply(this, finalArgs); }); this.describeCommand(name, `Smart command: ${signature}`, `Expects: ${signature}`); return this; } remainingTokens(n = 10) { return this.tokens.slice(this.pos, this.pos + n).join(' '); } } // ——— Conditional Kits ——— Yip.defineKit('if condition', function ({ chain = true } = {}) { const cond = this.parseParen(); const block = this.parseBlock(); const node = new CommandNode('if'); node.args = [new LiteralNode(cond), new LiteralNode(block)]; if (chain && this.peek() === 'else') { this.expect('else'); const elseBlock = this.parseBlock(); node.args.push(new LiteralNode(elseBlock)); } return node; }); Yip.defineKit('unless condition', function () { const cond = this.parseParen(); const block = this.parseBlock(); const node = new CommandNode('unless'); node.args = [new LiteralNode(cond), new LiteralNode(block)]; return node; }); // ——— Loop Kits ——— Yip.defineKit('repeat times', function () { const count = this.next(); // could also use parseParen() const block = this.parseBlock(); const node = new CommandNode('repeat'); node.args = [new LiteralNode(count), new LiteralNode(block)]; return node; }); Yip.defineKit('for-in', function () { const varName = this.next(); // e.g. i this.expect('in'); const iterable = this.next(); // e.g. list const block = this.parseBlock(); const node = new CommandNode('forin'); node.args = [new LiteralNode(varName), new LiteralNode(iterable), new LiteralNode(block)]; return node; }); // ——— Function Kits ——— Yip.defineKit('function def', function () { const name = this.next(); const params = this.parseParen().split(/\s*,\s*/); const block = this.parseBlock(); const node = new CommandNode('function'); node.args = [new LiteralNode(name), new LiteralNode(params), new LiteralNode(block)]; return node; }); Yip.defineKit('call fn', function () { const name = this.next(); const args = this.parseParen().split(/\s*,\s*/); const node = new CommandNode('call'); node.args = [new LiteralNode(name), new LiteralNode(args)]; return node; }); // ——— Declaration Kits ——— Yip.defineKit('let decl', function () { const decls = []; this.semicolonEnding(() => { const name = this.next(); this.expect('='); const val = this.next(); decls.push([name, val]); }); const node = new CommandNode('let'); node.args = decls.map(([name, val]) => new LiteralNode(`${name}=${val}`)); return node; }); // ——— Print, Log, Assert ——— Yip.defineKit('log value', function () { const val = this.next(); const node = new CommandNode('log'); node.args = [new LiteralNode(val)]; return node; }); Yip.defineKit('assert condition', function () { const cond = this.next(); const node = new CommandNode('assert'); node.args = [new LiteralNode(cond)]; return node; }); class YipExpresionEvaluator { constructor(yip) { this.yip = yip; this.operators = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, '/': (a, b) => a / b, '%': (a, b) => a % b, '**': (a, b) => Math.pow(a, b), }; } // basic math evaluator evaluate(expr) { const tokens = expr.split(/\s+/).filter(Boolean); const stack = []; for (const token of tokens) { if (this.operators[token]) { const b = stack.pop(); const a = stack.pop(); stack.push(this.operators[token](+a, +b)); } else { stack.push(token); } } return stack[0]; } // advanced expression evaluator (e.g., with variables) evaluateAdvanced(expr) { const tokens = expr.split(/\s+/).filter(Boolean); const stack = []; const evaluator = new YipExpresionEvaluator(this.yip); for (const token of tokens) { if (this.operators[token]) { const b = stack.pop(); const a = stack.pop(); stack.push(evaluator.operators[token](+a, +b)); } else if (token in this.yip.vars) { stack.push(this.yip.vars[token]); } else { stack.push(token); } } return stack[0]; } } module.exports = { Yip, ASTNode, ProgramNode, CommandNode, LiteralNode, YipTokenizer, YipError, YipExpresionEvaluator };