minangscript
Version:
Modern programming language with Minangkabau philosophy. Features native arrays (kumpulan), objects (benda), web development support, and comprehensive algorithm examples. Ready for web applications, data structures, and algorithmic programming.
633 lines (531 loc) • 18.6 kB
JavaScript
const { MinangLexer } = require('../lexer/MinangLexer');
class MinangParser {
constructor() {
this.lexer = new MinangLexer();
this.tokens = [];
this.current = 0;
}
parse(input) {
this.tokens = this.lexer.tokenize(input);
this.current = 0;
return this.program();
}
// Get current token
getCurrentToken() {
return this.tokens[this.current] || { type: 'EOF', value: null };
}
// Move to next token
advance() {
if (this.current < this.tokens.length - 1) {
this.current++;
}
return this.getCurrentToken();
}
// Check if current token matches expected type
match(expectedType) {
const token = this.getCurrentToken();
return token.type === expectedType;
}
// Consume token if it matches, otherwise throw error
consume(expectedType, errorMessage) {
const token = this.getCurrentToken();
if (token.type === expectedType) {
this.advance();
return token;
}
throw new Error(`${errorMessage}. Ditemukan ${token.type} pada baris ${token.line}`);
}
// Skip newlines
skipNewlines() {
while (this.match('NEWLINE')) {
this.advance();
}
}
// Parse program (top-level statements)
program() {
const statements = [];
this.skipNewlines();
while (!this.match('EOF')) {
if (this.match('NEWLINE')) {
this.advance();
continue;
}
statements.push(this.statement());
this.skipNewlines();
}
return {
type: 'Program',
body: statements
};
}
// Parse statement
statement() {
if (this.match('VAR') || this.match('LET') || this.match('CONST')) {
return this.variableDeclaration();
}
if (this.match('FUNCTION')) {
return this.functionDeclaration();
}
if (this.match('IF')) {
return this.ifStatement();
}
if (this.match('WHILE')) {
return this.whileStatement();
}
if (this.match('FOR')) {
return this.forStatement();
}
if (this.match('RETURN')) {
return this.returnStatement();
}
if (this.match('PRINT')) {
// Check if it's a simple print or member expression
if (this.tokens[this.current + 1] && this.tokens[this.current + 1].type === 'DOT') {
return this.printMemberStatement(); // Handle as print member expression
}
return this.printStatement();
}
if (this.match('LBRACE')) {
return this.blockStatement();
}
// Expression statement
return this.expressionStatement();
}
// Variable declaration: buek/ambiak/tagak nama = nilai
variableDeclaration() {
const declarationType = this.getCurrentToken().type; // VAR, LET, or CONST
const keyword = this.getCurrentToken().value;
this.advance(); // consume VAR/LET/CONST
const name = this.consume('IDENTIFIER', 'Diharapkan nama variabel').value;
let initializer = null;
if (this.match('ASSIGN')) {
this.advance();
initializer = this.expression();
} else if (declarationType === 'CONST') {
throw new Error('Konstanta harus diinisialisasi dengan nilai');
}
return {
type: 'VariableDeclaration',
declarationType: declarationType,
identifier: name,
initializer: initializer
};
}
// Function declaration: karojo nama(params) { body }
functionDeclaration() {
this.consume('FUNCTION', 'Diharapkan kata kunci "karojo"');
const name = this.consume('IDENTIFIER', 'Diharapkan nama fungsi').value;
this.consume('LPAREN', 'Diharapkan "(" setelah nama fungsi');
const params = [];
if (!this.match('RPAREN')) {
do {
const param = this.consume('IDENTIFIER', 'Diharapkan nama parameter').value;
params.push(param);
if (this.match('COMMA')) {
this.advance();
} else {
break;
}
} while (!this.match('RPAREN'));
}
this.consume('RPAREN', 'Diharapkan ")" setelah parameter');
const body = this.blockStatement();
return {
type: 'FunctionDeclaration',
name: name,
params: params,
body: body
};
}
// If statement: kalau (condition) { body } lain { elseBody }
ifStatement() {
this.consume('IF', 'Diharapkan kata kunci "kalau"');
const condition = this.expression();
const consequent = this.blockStatement();
let alternate = null;
if (this.match('ELSE')) {
this.advance();
if (this.match('IF')) {
alternate = this.ifStatement(); // Handle else if
} else {
alternate = this.blockStatement();
}
}
return {
type: 'IfStatement',
condition: condition,
consequent: consequent,
alternate: alternate
};
}
// While statement: selamo (condition) { body }
whileStatement() {
this.consume('WHILE', 'Diharapkan kata kunci "selamo"');
const condition = this.expression();
const body = this.blockStatement();
return {
type: 'WhileStatement',
condition: condition,
body: body
};
}
// For statement: untuak (init; condition; update) { body }
forStatement() {
this.consume('FOR', 'Diharapkan kata kunci "untuak"');
this.consume('LPAREN', 'Diharapkan "(" setelah "untuak"');
const init = this.match('SEMICOLON') ? null : this.statement();
this.consume('SEMICOLON', 'Diharapkan ";" setelah inisialisasi for');
const condition = this.match('SEMICOLON') ? null : this.expression();
this.consume('SEMICOLON', 'Diharapkan ";" setelah kondisi for');
const update = this.match('RPAREN') ? null : this.expression();
this.consume('RPAREN', 'Diharapkan ")" setelah update for');
const body = this.blockStatement();
return {
type: 'ForStatement',
init: init,
condition: condition,
update: update,
body: body
};
}
// Return statement: jadi expression
returnStatement() {
this.consume('RETURN', 'Diharapkan kata kunci "jadi"');
let value = null;
if (!this.match('NEWLINE') && !this.match('EOF')) {
value = this.expression();
}
return {
type: 'ReturnStatement',
argument: value
};
}
// Print statement: cetak expression
printStatement() {
this.consume('PRINT', 'Diharapkan kata kunci "cetak"');
const argument = this.expression();
return {
type: 'PrintStatement',
argument: argument
};
}
// Print member statement: cetak.rusak expression (without parentheses)
printMemberStatement() {
this.consume('PRINT', 'Diharapkan kata kunci "cetak"');
this.consume('DOT', 'Diharapkan "." setelah "cetak"');
const property = this.consume('IDENTIFIER', 'Diharapkan nama method setelah "."').value;
const argument = this.expression();
return {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'cetak'
},
property: property
},
arguments: [argument]
}
};
}
// Block statement: { statements }
blockStatement() {
this.consume('LBRACE', 'Diharapkan "{"');
this.skipNewlines();
const statements = [];
while (!this.match('RBRACE') && !this.match('EOF')) {
if (this.match('NEWLINE')) {
this.advance();
continue;
}
statements.push(this.statement());
this.skipNewlines();
}
this.consume('RBRACE', 'Diharapkan "}"');
return {
type: 'BlockStatement',
body: statements
};
}
// Expression statement
expressionStatement() {
const expr = this.expression();
return {
type: 'ExpressionStatement',
expression: expr
};
}
// Parse expression (assignment level)
expression() {
return this.assignment();
}
// Assignment expression
assignment() {
let expr = this.logicalOr();
if (this.match('ASSIGN')) {
this.advance();
const value = this.assignment();
if (expr.type === 'Identifier') {
return {
type: 'AssignmentExpression',
left: expr,
right: value
};
}
throw new Error('Target assignment tidak valid');
}
return expr;
}
// Logical OR expression
logicalOr() {
let expr = this.logicalAnd();
while (this.match('OR')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.logicalAnd();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Logical AND expression
logicalAnd() {
let expr = this.equality();
while (this.match('AND')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.equality();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Equality expression
equality() {
let expr = this.comparison();
while (this.match('EQUAL') || this.match('NOT_EQUAL')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.comparison();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Comparison expression
comparison() {
let expr = this.addition();
while (this.match('GREATER_THAN') || this.match('GREATER_EQUAL') ||
this.match('LESS_THAN') || this.match('LESS_EQUAL')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.addition();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Addition and subtraction
addition() {
let expr = this.multiplication();
while (this.match('PLUS') || this.match('MINUS')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.multiplication();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Multiplication, division, modulo
multiplication() {
let expr = this.unary();
while (this.match('MULTIPLY') || this.match('DIVIDE') || this.match('MODULO')) {
const operator = this.getCurrentToken().value;
this.advance();
const right = this.unary();
expr = {
type: 'BinaryExpression',
left: expr,
operator: operator,
right: right
};
}
return expr;
}
// Unary expression
unary() {
if (this.match('NOT') || this.match('MINUS')) {
const operator = this.getCurrentToken().value;
this.advance();
const expr = this.unary();
return {
type: 'UnaryExpression',
operator: operator,
argument: expr
};
}
return this.call();
}
// Function call expression
call() {
let expr = this.primary();
while (true) {
if (this.match('LPAREN')) {
this.advance();
const args = [];
if (!this.match('RPAREN')) {
do {
args.push(this.expression());
if (this.match('COMMA')) {
this.advance();
} else {
break;
}
} while (!this.match('RPAREN'));
}
this.consume('RPAREN', 'Diharapkan ")" setelah argumen');
expr = {
type: 'CallExpression',
callee: expr,
arguments: args
};
} else if (this.match('DOT')) {
// Handle member access like cetak.rusak
this.advance();
const property = this.consume('IDENTIFIER', 'Diharapkan nama properti setelah "."').value;
expr = {
type: 'MemberExpression',
object: expr,
property: property
};
} else {
break;
}
}
return expr;
}
// Primary expression
primary() {
if (this.match('TRUE')) {
this.advance();
return { type: 'Literal', value: true };
}
if (this.match('FALSE')) {
this.advance();
return { type: 'Literal', value: false };
}
if (this.match('NULL')) {
this.advance();
return { type: 'Literal', value: null };
}
if (this.match('NUMBER')) {
const value = this.getCurrentToken().value;
this.advance();
return { type: 'Literal', value: value };
}
if (this.match('STRING')) {
const value = this.getCurrentToken().value;
this.advance();
return { type: 'Literal', value: value };
}
if (this.match('IDENTIFIER')) {
const name = this.getCurrentToken().value;
this.advance();
return { type: 'Identifier', name: name };
}
// Handle PRINT as identifier for member expressions
if (this.match('PRINT')) {
const name = this.getCurrentToken().value;
this.advance();
return { type: 'Identifier', name: name };
}
// Handle cultural keywords as identifiers in expressions
if (this.match('COLLABORATE') || this.match('DISCUSS') ||
this.match('LEARN') || this.match('ETHICAL')) {
const name = this.getCurrentToken().value;
this.advance();
return { type: 'Identifier', name: name };
}
// Array literal: kumpulan[element1, element2, ...]
if (this.match('ARRAY_LITERAL')) {
this.advance();
this.consume('LBRACKET', 'Diharapkan "[" setelah "kumpulan"');
const elements = [];
if (!this.match('RBRACKET')) {
do {
elements.push(this.expression());
} while (this.match('COMMA') && this.advance());
}
this.consume('RBRACKET', 'Diharapkan "]" setelah elemen array');
return {
type: 'ArrayExpression',
elements: elements
};
}
// Object literal: benda{key1: value1, key2: value2, ...}
if (this.match('OBJECT_LITERAL')) {
this.advance();
this.consume('LBRACE', 'Diharapkan "{" setelah "benda"');
const properties = [];
if (!this.match('RBRACE')) {
do {
let key;
if (this.match('IDENTIFIER')) {
key = this.getCurrentToken().value;
this.advance();
} else if (this.match('STRING')) {
key = this.getCurrentToken().value;
this.advance();
} else {
throw new Error('Diharapkan nama properti atau string');
}
this.consume('COLON', 'Diharapkan ":" setelah nama properti');
const value = this.expression();
properties.push({
type: 'Property',
key: key,
value: value
});
} while (this.match('COMMA') && this.advance());
}
this.consume('RBRACE', 'Diharapkan "}" setelah properti objek');
return {
type: 'ObjectExpression',
properties: properties
};
}
if (this.match('LPAREN')) {
this.advance();
const expr = this.expression();
this.consume('RPAREN', 'Diharapkan ")" setelah ekspresi');
return expr;
}
const token = this.getCurrentToken();
throw new Error(`Token tidak terduga ${token.type} pada baris ${token.line}`);
}
}
module.exports = { MinangParser };