✨ parser code
This commit is contained in:
parent
2d7a85f6c9
commit
d7dbbe4845
@ -35,7 +35,7 @@ class Juicescript {
|
|||||||
|
|
||||||
// ARGUMENT TYPES //
|
// ARGUMENT TYPES //
|
||||||
static argument_type = new Juicescript_helper_enum(
|
static argument_type = new Juicescript_helper_enum(
|
||||||
"VARIABLE", "LITERAL"
|
"VARIABLE", "LITERAL", "OPERATOR"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +79,6 @@ class Juicescript {
|
|||||||
|
|
||||||
// run lexical analysis
|
// run lexical analysis
|
||||||
let token_list = lexer.scan();
|
let token_list = lexer.scan();
|
||||||
/**/console.log(token_list);
|
|
||||||
|
|
||||||
// stop here if unsuccessful
|
// stop here if unsuccessful
|
||||||
if(lexer.error_count > 0) return false;
|
if(lexer.error_count > 0) return false;
|
||||||
|
383
src/parser.js
383
src/parser.js
@ -22,7 +22,7 @@ class Juicescript_parser {
|
|||||||
// current scope
|
// current scope
|
||||||
this.scope = null;
|
this.scope = null;
|
||||||
|
|
||||||
// token list
|
// program tree
|
||||||
this.tree = this.program_tree_template();
|
this.tree = this.program_tree_template();
|
||||||
|
|
||||||
// warning and error counter
|
// warning and error counter
|
||||||
@ -61,7 +61,7 @@ class Juicescript_parser {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
// END //
|
// `END` //
|
||||||
case Juicescript.token_type.END:
|
case Juicescript.token_type.END:
|
||||||
this.parse_end();
|
this.parse_end();
|
||||||
break;
|
break;
|
||||||
@ -73,6 +73,18 @@ class Juicescript_parser {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
// FLAG //
|
||||||
|
case Juicescript.token_type.FLAG:
|
||||||
|
this.parse_flag();
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
// `GLOBAL` //
|
||||||
|
case Juicescript.token_type.GLOBAL:
|
||||||
|
this.parse_global();
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
// EVERYTHING ELSE //
|
// EVERYTHING ELSE //
|
||||||
default:
|
default:
|
||||||
// unexpected (ignore with error)
|
// unexpected (ignore with error)
|
||||||
@ -85,66 +97,148 @@ class Juicescript_parser {
|
|||||||
PARSER: Handle own command definition
|
PARSER: Handle own command definition
|
||||||
*/
|
*/
|
||||||
parse_def(){
|
parse_def(){
|
||||||
let parameter_list = [];
|
// GET COMMAND NAME //
|
||||||
while(!this.is_at_end()){
|
// consume name
|
||||||
// IS NEXT TOKEN A DELIMITER? //
|
|
||||||
if(this.match_type(Juicescript.token_type.DELIMITER)){
|
|
||||||
// end of parameter list
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// GET NEXT PARAMETER //
|
|
||||||
// consume next token
|
|
||||||
this.next();
|
this.next();
|
||||||
/**/continue;
|
|
||||||
|
|
||||||
// make sure next has valid type for parameter
|
// make sure token has valid type
|
||||||
if(!this.is_parameter(this.token)){
|
let name = null;
|
||||||
|
if(this.token.type === Juicescript.token_type.IDENTIFIER){
|
||||||
|
// get from identifier value
|
||||||
|
name = this.token.value;
|
||||||
|
|
||||||
|
} else {
|
||||||
// ignore with error
|
// ignore with error
|
||||||
this.error_token("invalid parameter");
|
this.error_token("expected identifier, but got");
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// PARSE PARAMETER //
|
// GET LIST OF PARAMETERS //
|
||||||
// get parsed object
|
let parameter_list = this.parse_parameter_list();
|
||||||
let parameter = this.parse_parameter();
|
|
||||||
|
|
||||||
// add to list
|
|
||||||
parameter_list.push(parameter);
|
// CAN ONLY BE USED IN ROOT SCOPE //
|
||||||
|
if(this.scope !== null){
|
||||||
|
// ignore with error
|
||||||
|
this.error("'def' can not be used inside a command definition");
|
||||||
}
|
}
|
||||||
/**/console.log(parameter_list);
|
|
||||||
|
|
||||||
|
|
||||||
|
// COMMAND NAME MUST BE UNIQUE //
|
||||||
|
if(Object.keys(this.tree.scope).includes(name)){
|
||||||
|
// ignore with error
|
||||||
|
this.error("connot redefine command '" + name + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// INITIALIZE SCOPE //
|
||||||
|
// ignore if name is invalid
|
||||||
|
if(name === null) return;
|
||||||
|
|
||||||
|
// enter this command's scope
|
||||||
|
this.scope = name;
|
||||||
|
|
||||||
|
// create empty scope from template
|
||||||
|
this.scope_tree = this.scope_tree_template();
|
||||||
|
|
||||||
/**/this.debug("enter scope " + this.peek().value);
|
// store parameter list
|
||||||
/**/this.scope = this.peek().value;
|
this.scope_tree.parameter = parameter_list;
|
||||||
/**/this.tree.scope[this.scope] = this.scope_tree_template();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
PARSER: Handle `end` command
|
PARSER: Handle `end` command
|
||||||
*/
|
*/
|
||||||
parse_end(){
|
parse_end(){
|
||||||
/**/this.debug("enter scope root");
|
// ARE WE IN ROOT SCOPE? //
|
||||||
/**/this.scope = null;
|
if(this.scope === null){
|
||||||
|
// add as command
|
||||||
|
this.command_add({name: "end"});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// EXIT FROM SCOPE //
|
||||||
|
this.scope = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
PARSER: Handle command
|
PARSER: Handle command
|
||||||
*/
|
*/
|
||||||
parse_command(){
|
parse_command(){
|
||||||
|
// GET COMMAND NAME //
|
||||||
|
let command = this.token.value;
|
||||||
|
|
||||||
|
|
||||||
// GET LIST OF ARGUMENTS //
|
// GET LIST OF ARGUMENTS //
|
||||||
let argument_list = this.parse_argument_list();
|
let argument_list = this.parse_argument_list();
|
||||||
/**/console.log(argument_list);
|
|
||||||
|
|
||||||
|
|
||||||
///**/this.command_add({name: this.token.value});
|
// ADD TO LIST OF COMMANDS //
|
||||||
|
this.command_add({name: command, argument: argument_list});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PARSER: Handle flag
|
||||||
|
*/
|
||||||
|
parse_flag(){
|
||||||
|
// GET FLAG NAME //
|
||||||
|
let flag = this.token.value;
|
||||||
|
|
||||||
|
|
||||||
|
// MAKE SURE THE FLAG NAME IS UNIQUE //
|
||||||
|
if(Object.keys(this.scope_tree.flag).includes(flag)){
|
||||||
|
// ignore with error
|
||||||
|
this.error("can not redefine flag '" + flag + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ADD TO LIST //
|
||||||
|
// get current scope's command counter
|
||||||
|
let scope_command_count = this.scope_tree.command.length;
|
||||||
|
|
||||||
|
// store in list
|
||||||
|
this.scope_tree.flag[flag] = {line: this.token.line, command_next: scope_command_count};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PARSER: Handle `global` command
|
||||||
|
*/
|
||||||
|
parse_global(){
|
||||||
|
// GET LIST OF PARAMETERS //
|
||||||
|
let parameter_list = this.parse_parameter_list({strict: true});
|
||||||
|
|
||||||
|
|
||||||
|
// MAKE SURE WE ARE INSIDE OF A SCOPE //
|
||||||
|
if(this.scope === null){
|
||||||
|
// ignore with error
|
||||||
|
this.error("'global' can only be used inside a command definition");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MAKE SURE THEY ARE DEFINED BEFORE ANY COMMANDS //
|
||||||
|
if(this.scope_tree.command.length > 0){
|
||||||
|
// ignore with error
|
||||||
|
this.error("'global' must come before any other command");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ADD TO LIST OF GLOBAL VARIABLES //
|
||||||
|
for(var one_parameter of parameter_list){
|
||||||
|
// get variable name from parameter
|
||||||
|
let name = one_parameter.name;
|
||||||
|
|
||||||
|
// check if already in the list
|
||||||
|
if(this.scope_tree.global.includes(name)){
|
||||||
|
// ignore with warning
|
||||||
|
this.warning("variable '" + name + "' is already global");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// add to list
|
||||||
|
this.scope_tree.global.push(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -164,10 +258,10 @@ class Juicescript_parser {
|
|||||||
// consume next token
|
// consume next token
|
||||||
this.next();
|
this.next();
|
||||||
|
|
||||||
// make sure next has valid type for an argument
|
// make sure next has valid type for an argument (also allow operators)
|
||||||
if(!this.is_argument(this.token)){
|
if(!this.is_argument(this.token, {operator: true})){
|
||||||
// ignore with error
|
// ignore with error
|
||||||
this.error_token("invalid argument");
|
this.error_token("expected argument, but got");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,26 +283,126 @@ class Juicescript_parser {
|
|||||||
PARSER: Handle argument at current position and return resulting object
|
PARSER: Handle argument at current position and return resulting object
|
||||||
*/
|
*/
|
||||||
parse_argument(){
|
parse_argument(){
|
||||||
// NEW OJECT //
|
// VARIABLE? //
|
||||||
let argument = {};
|
|
||||||
|
|
||||||
|
|
||||||
// VARIABLE OR LITERAL? //
|
|
||||||
if(this.is_variable(this.token)){
|
if(this.is_variable(this.token)){
|
||||||
// parse variable
|
// return object
|
||||||
let variable = this.parse_variable();
|
return {type: Juicescript.argument_type.VARIABLE, variable: this.parse_variable()};
|
||||||
|
}
|
||||||
|
|
||||||
// add to list
|
|
||||||
argument = {type: Juicescript.argument_type.VARIABLE, variable: variable};
|
|
||||||
|
|
||||||
} else {
|
// OPERATOR? //
|
||||||
// add literal
|
if(this.is_operator(this.token)){
|
||||||
argument = {type: Juicescript.argument_type.LITERAL, value: this.token.value};
|
// return object
|
||||||
|
return {type: Juicescript.argument_type.OPERATOR, operator: this.token.type};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// LITERAL? //
|
||||||
|
if(this.is_literal(this.token)){
|
||||||
|
// return object
|
||||||
|
return {type: Juicescript.argument_type.LITERAL, value: this.token.value};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// UNKNOWN //
|
||||||
|
throw "called parse_argument() on non-argument token";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PARSER: Handle parameter list at current position with OPTIONS and return resulting list of objects
|
||||||
|
*/
|
||||||
|
parse_parameter_list(options = {}){
|
||||||
|
// SET DEFAULTS FOR OPTIONS //
|
||||||
|
options.strict ??= false;
|
||||||
|
|
||||||
|
|
||||||
|
let parameter_list = [];
|
||||||
|
while(!this.is_at_end()){
|
||||||
|
// IS NEXT TOKEN A DELIMITER? //
|
||||||
|
if(this.match_type(Juicescript.token_type.DELIMITER)){
|
||||||
|
// end of parameter list
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GET NEXT PARAMETER //
|
||||||
|
// clear stuff
|
||||||
|
let reference = false;
|
||||||
|
let optional = false;
|
||||||
|
|
||||||
|
// consume next token
|
||||||
|
this.next();
|
||||||
|
|
||||||
|
|
||||||
|
// CHECK FOR AMPERSAND PREFIX //
|
||||||
|
if(this.token.type === Juicescript.token_type.AMPERSAND){
|
||||||
|
// if not allowed, ignore with error
|
||||||
|
if(options.strict) this.error_token("modifier not allowed:");
|
||||||
|
|
||||||
|
// remember
|
||||||
|
reference = true;
|
||||||
|
|
||||||
|
// consume
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MAKE SURE TOKEN HAS VALID TYPE FOR A PARAMETER //
|
||||||
|
if(!this.is_variable(this.token)){
|
||||||
|
// ignore with error
|
||||||
|
this.error_token("expected parameter, but got");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GET PARAMETER NAME //
|
||||||
|
let parameter_name = this.parse_parameter();
|
||||||
|
|
||||||
|
|
||||||
|
// CHECK FOR QUESTION MARK SUFFIX //
|
||||||
|
if(this.match_type(Juicescript.token_type.QUESTION_MARK)){
|
||||||
|
// if not allowed, ignore with error
|
||||||
|
if(options.strict) this.error_token("modifier not allowed:");
|
||||||
|
|
||||||
|
// remember
|
||||||
|
optional = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ADD TO LIST //
|
||||||
|
// ignore if parsing of name was unsuccessful
|
||||||
|
if(parameter_name === null) continue;
|
||||||
|
|
||||||
|
// build object
|
||||||
|
let parameter = {name: parameter_name, reference: reference, optional: optional};
|
||||||
|
|
||||||
|
// add
|
||||||
|
parameter_list.push(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// RETURN //
|
// RETURN //
|
||||||
return argument;
|
return parameter_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PARSER: Handle parameter at current position and return its name
|
||||||
|
*/
|
||||||
|
parse_parameter(){
|
||||||
|
// MAKE SURE VARIABLE HAS A DIRECT NAME //
|
||||||
|
if(this.token.value.length <= 0){
|
||||||
|
// ignore with error
|
||||||
|
this.error_token("expected parameter name, but got", this.peek());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RETURN NAME //
|
||||||
|
// get name
|
||||||
|
let name = this.token.value;
|
||||||
|
|
||||||
|
// return
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -231,7 +425,7 @@ class Juicescript_parser {
|
|||||||
// consume argument
|
// consume argument
|
||||||
this.next();
|
this.next();
|
||||||
|
|
||||||
// make sure token has valid type for an argument
|
// make sure token has valid type for variable name
|
||||||
if(this.is_argument(this.token)){
|
if(this.is_argument(this.token)){
|
||||||
// value of another variable
|
// value of another variable
|
||||||
let argument = this.parse_argument();
|
let argument = this.parse_argument();
|
||||||
@ -259,7 +453,7 @@ class Juicescript_parser {
|
|||||||
// consume argument
|
// consume argument
|
||||||
this.next();
|
this.next();
|
||||||
|
|
||||||
// make sure token has valid type for an argument
|
// make sure token has valid type for list index
|
||||||
if(this.is_argument(this.token)){
|
if(this.is_argument(this.token)){
|
||||||
// parse argument
|
// parse argument
|
||||||
let argument = this.parse_argument();
|
let argument = this.parse_argument();
|
||||||
@ -284,6 +478,36 @@ class Juicescript_parser {
|
|||||||
return variable;
|
return variable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GETTER / SETTER: Return tree of current scope
|
||||||
|
*/
|
||||||
|
get scope_tree(){
|
||||||
|
// are we in root scope?
|
||||||
|
if(this.scope === null){
|
||||||
|
// return root scope
|
||||||
|
return this.tree.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure this scope exists
|
||||||
|
if(!Object.keys(this.tree.scope).includes(this.scope)){
|
||||||
|
throw "unknown scope " + this.scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return scope
|
||||||
|
return this.tree.scope[this.scope];
|
||||||
|
}
|
||||||
|
set scope_tree(value){
|
||||||
|
// are we in root scope?
|
||||||
|
if(this.scope === null){
|
||||||
|
// set root scope
|
||||||
|
this.tree.root = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set scope
|
||||||
|
this.tree.scope[this.scope] = value;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HELPER: Consume next token from list
|
HELPER: Consume next token from list
|
||||||
*/
|
*/
|
||||||
@ -339,16 +563,15 @@ class Juicescript_parser {
|
|||||||
/*
|
/*
|
||||||
HELPER: Check if type of TOKEN is valid for arguments
|
HELPER: Check if type of TOKEN is valid for arguments
|
||||||
*/
|
*/
|
||||||
is_argument(token){
|
is_argument(token, options = {}){
|
||||||
// check lookup table
|
// SET DEFAULTS FOR OPTIONS //
|
||||||
return ([
|
options.operator ??= false;
|
||||||
Juicescript.token_type.VARIABLE,
|
|
||||||
Juicescript.token_type.STRING,
|
|
||||||
Juicescript.token_type.NUMBER,
|
// CHECK //
|
||||||
Juicescript.token_type.TRUE,
|
return this.is_variable(token) ||
|
||||||
Juicescript.token_type.FALSE,
|
(options.operator && this.is_operator(token)) ||
|
||||||
Juicescript.token_type.NULL
|
this.is_literal(token);
|
||||||
]).includes(token.type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -358,6 +581,36 @@ class Juicescript_parser {
|
|||||||
return (token.type === Juicescript.token_type.VARIABLE);
|
return (token.type === Juicescript.token_type.VARIABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HELPER: Check if TOKEN is an operator
|
||||||
|
*/
|
||||||
|
is_operator(token){
|
||||||
|
// check lookup table
|
||||||
|
return ([
|
||||||
|
Juicescript.token_type.NOT,
|
||||||
|
Juicescript.token_type.EQUAL_EQUAL,
|
||||||
|
Juicescript.token_type.NOT_EQUAL,
|
||||||
|
Juicescript.token_type.GREATER,
|
||||||
|
Juicescript.token_type.GREATER_EQUAL,
|
||||||
|
Juicescript.token_type.LESS,
|
||||||
|
Juicescript.token_type.LESS_EQUAL
|
||||||
|
]).includes(token.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
HELPER: Check if TOKEN is a literal
|
||||||
|
*/
|
||||||
|
is_literal(token){
|
||||||
|
// check lookup table
|
||||||
|
return ([
|
||||||
|
Juicescript.token_type.STRING,
|
||||||
|
Juicescript.token_type.NUMBER,
|
||||||
|
Juicescript.token_type.TRUE,
|
||||||
|
Juicescript.token_type.FALSE,
|
||||||
|
Juicescript.token_type.NULL
|
||||||
|
]).includes(token.type);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HELPER: Add new command object with OPTIONS to current scope
|
HELPER: Add new command object with OPTIONS to current scope
|
||||||
*/
|
*/
|
||||||
@ -384,12 +637,11 @@ class Juicescript_parser {
|
|||||||
|
|
||||||
|
|
||||||
// ADD TO SCOPE'S COMMAND LIST //
|
// ADD TO SCOPE'S COMMAND LIST //
|
||||||
/**/this.debug("add command " + command.name + " to scope " + (this.scope ?? "root"));
|
// get scope command list
|
||||||
/**/console.log(this.scope, command);
|
let scope_command_list = this.scope_tree.command;
|
||||||
/**/let scope_command_list = (this.scope === null ? this.tree.root.command : this.tree.scope[this.scope].command);
|
|
||||||
/**/scope_command_list ??= [];
|
// add command object
|
||||||
/**/scope_command_list.push(command);
|
scope_command_list.push(command);
|
||||||
//*/ TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -411,6 +663,7 @@ class Juicescript_parser {
|
|||||||
*/
|
*/
|
||||||
scope_tree_template(){
|
scope_tree_template(){
|
||||||
return {
|
return {
|
||||||
|
parameter: [],
|
||||||
command: [],
|
command: [],
|
||||||
flag: {},
|
flag: {},
|
||||||
global: []
|
global: []
|
||||||
|
Reference in New Issue
Block a user