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
JavaScript
// 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 };