This repository has been archived on 2024-02-25. You can view files and clone it, but cannot push or open issues or pull requests.
Wol/addons/Wol/core/compiler/Compiler.gd

466 lines
14 KiB
GDScript

extends Object
signal error(message, line_number, column)
const Constants = preload('res://addons/Wol/core/Constants.gd')
const Lexer = preload('res://addons/Wol/core/compiler/Lexer.gd')
const Program = preload('res://addons/Wol/core/Program.gd')
const Parser = preload('res://addons/Wol/core/compiler/Parser.gd')
const INVALID_TITLE = '[\\[<>\\]{}\\|:\\s#\\$]'
var source = ''
var filename = ''
var current_node
var has_implicit_string_tags = false
var soft_assert = false
var string_count = 0
var string_table = {}
var label_count = 0
func _init(_filename, _source = null, _soft_assert = false):
filename = _filename
soft_assert = _soft_assert
if not _filename and _source:
filename = 'inline_source'
source = _source
else:
var file = File.new()
file.open(_filename, File.READ)
source = file.get_as_text()
file.close()
var source_lines = source.split('\n')
for i in range(source_lines.size()):
source_lines[i] = source_lines[i].strip_edges(false, true)
source = source_lines.join('\n')
func get_headers(offset = 0):
var header_property = RegEx.new()
var header_sep = RegEx.new()
header_sep.compile('---(\r\n|\r|\n)')
header_property.compile('(?<field>.*): *(?<value>.*)')
self.assert(header_sep.search(source), 'No headers found!')
var title = ''
var position = Vector2.ZERO
var source_lines = source.split('\n')
var line_number = offset
while line_number < source_lines.size():
var line = source_lines[line_number]
line_number += 1
if not line.empty():
var result = header_property.search(line)
if result != null:
var field = result.get_string('field')
var value = result.get_string('value')
if field == 'title':
var regex = RegEx.new()
regex.compile(INVALID_TITLE)
self.assert(not regex.search(value), 'Invalid characters in title "%s", correct to "%s"' % [value, regex.sub(value, '', true)])
title = value
if field == 'position':
var regex = RegEx.new()
regex.compile('^position:.*,.*\\d$')
self.assert(regex.search(line), 'Couldn\'t parse position property in the headers, got "%s" instead in node "%s"' % [value, title])
position = Vector2(int(value.split(',')[0].strip_edges()), int(value.split(',')[1].strip_edges()))
# TODO: Implement color and tags
if line == '---':
break
return {
'title': title,
'position': position
}
func get_body(offset = 0):
var body_lines = []
var source_lines = source.split('\n')
var recording = false
var line_number = offset
while line_number < source_lines.size() and source_lines[line_number] != '===':
if recording:
body_lines.append(source_lines[line_number])
recording = recording or source_lines[line_number] == '---'
line_number += 1
line_number += 1
return PoolStringArray(body_lines).join('\n')
func get_nodes():
var nodes = []
var line_number = 0
var source_lines = source.split('\n')
while line_number < source_lines.size():
var headers = get_headers(line_number)
var body = get_body(line_number)
headers.body = body
nodes.append(headers)
# Add +2 to the final line to skip the === from that node
line_number = Array(source_lines).find_last(body.split('\n')[-1]) + 2
while line_number < source_lines.size() and source_lines[line_number].empty():
line_number += 1
return nodes
func assert(statement, message, line_number = -1, column = -1, _absolute_line_number = -1):
if not soft_assert:
assert(statement, '"%s" on line %d column %d' % [message, line_number, column])
elif not statement:
emit_signal('error', message, line_number, column)
return not statement
func compile():
var parsed_nodes = []
for node in get_nodes():
var lexer = Lexer.new(self, filename, node.title, node.body)
var tokens = lexer.tokenize()
# In case of lexer error
if not tokens:
return
var parser = Parser.new(self, node.title, tokens)
var parser_node = parser.parse_node()
parser_node.name = node.title
parsed_nodes.append(parser_node)
var program = Program.new()
program.filename = filename
for node in parsed_nodes:
compile_node(program, node)
for key in string_table:
program.strings[key] = string_table[key]
return program
func compile_node(program, parsed_node):
self.assert(not program.nodes.has(parsed_node.name), 'Duplicate node in program: %s' % parsed_node.name)
var node_compiled = Program.WolNode.new()
node_compiled.name = parsed_node.name
node_compiled.tags = parsed_node.tags
if parsed_node.source != null and not parsed_node.source.empty():
node_compiled.source_id = register_string(
parsed_node.source,
parsed_node.name,
'line:' + parsed_node.name,
0,
[]
)
else:
var start_label = register_label()
emit(Constants.ByteCode.Label, node_compiled, [Program.Operand.new(start_label)])
for statement in parsed_node.statements:
generate_statement(node_compiled, statement)
var dangling_options = false
for instruction in node_compiled.instructions:
if instruction.operation == Constants.ByteCode.AddOption:
dangling_options = true
if instruction.operation == Constants.ByteCode.ShowOptions:
dangling_options = false
if dangling_options:
emit(Constants.ByteCode.ShowOptions, node_compiled)
emit(Constants.ByteCode.RunNode, node_compiled)
else:
emit(Constants.ByteCode.Stop, node_compiled)
program.nodes[node_compiled.name] = node_compiled
func register_string(text, node_name, id = '', line_number = -1, tags = []):
var line_id_used = ''
var implicit = false
if id.empty():
line_id_used = '%s-%s-%d' % [filename, node_name, string_count]
string_count += 1
#use this when we generate implicit tags
#they are not saved and are generated
#aka dummy tags that change on each compilation
has_implicit_string_tags = true
implicit = true
else :
line_id_used = id
implicit = false
var string_info = Program.Line.new(text, node_name, line_number, filename, implicit, tags)
string_table[line_id_used] = string_info
return line_id_used
func register_label(comment = ''):
label_count += 1
return 'Label%s%s' % [label_count, comment]
func emit(bytecode, node = current_node, operands = []):
var instruction = Program.Instruction.new(null)
instruction.operation = bytecode
instruction.operands = operands
if node == null:
printerr('Trying to emit to null node with byte_code: %s' % bytecode)
return
node.instructions.append(instruction)
if bytecode == Constants.ByteCode.Label:
node.labels[instruction.operands[0].value] = node.instructions.size() - 1
func generate_statement(node, statement):
match statement.type:
Constants.StatementTypes.CustomCommand:
generate_custom_command(node, statement.custom_command)
Constants.StatementTypes.ShortcutOptionGroup:
generate_shortcut_group(node, statement.shortcut_option_group)
Constants.StatementTypes.Block:
generate_block(node, statement.block.statements)
Constants.StatementTypes.IfStatement:
generate_if(node, statement.if_statement)
Constants.StatementTypes.OptionStatement:
generate_option(node, statement.option_statement)
Constants.StatementTypes.AssignmentStatement:
generate_assignment(node, statement.assignment)
Constants.StatementTypes.Line:
generate_line(node, statement)
_:
self.assert(false, statement.line_number, 'Illegal statement type [%s]. Could not generate code.' % statement.type)
func generate_custom_command(node, command):
# TODO: See if the first tree of this statement is being used
if command.expression != null:
generate_expression(node, command.expression)
else:
var command_string = command.client_command
if command_string == 'stop':
emit(Constants.ByteCode.Stop, node)
else :
emit(Constants.ByteCode.RunCommand, node, [Program.Operand.new(command_string)])
func generate_line(node, statement):
# TODO: Implement proper line numbers (global and local)
var line = statement.line
var expression_count = line.substitutions.size()
while not line.substitutions.empty():
var inline_expression = line.substitutions.pop_back()
generate_expression(node, inline_expression.expression)
var num = register_string(line.line_text, node.name, line.line_id, statement.line_number, line.tags);
emit(Constants.ByteCode.RunLine, node,[Program.Operand.new(num), Program.Operand.new(expression_count)])
func generate_shortcut_group(node, shortcut_group):
var end = register_label('group_end')
var labels = []
var option_count = 0
for option in shortcut_group.options:
var endof_clause = ''
var op_destination = register_label('option_%s' % [option_count + 1])
labels.append(op_destination)
if option.condition != null:
endof_clause = register_label('conditional_%s' % option_count)
generate_expression(node, option.condition)
emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(endof_clause)])
var label_line_id = '' #TODO: Add tag support
var label_string_id = register_string(
option.label,
node.name,
label_line_id,
option.line_number,
[]
)
emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(label_string_id), Program.Operand.new(op_destination)])
if option.condition != null:
emit(Constants.ByteCode.Label, node, [Program.Operand.new(endof_clause)])
emit(Constants.ByteCode.Pop, node)
option_count += 1
emit(Constants.ByteCode.ShowOptions, node)
emit(Constants.ByteCode.Jump, node)
option_count = 0
for option in shortcut_group.options:
emit(Constants.ByteCode.Label, node, [Program.Operand.new(labels[option_count])])
if option.node != null:
generate_block(node, option.node.statements)
emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(end)])
option_count += 1
emit(Constants.ByteCode.Label, node, [Program.Operand.new(end)])
emit(Constants.ByteCode.Pop, node)
func generate_block(node, statements = []):
if not statements.empty():
for statement in statements:
generate_statement(node, statement)
func generate_if(node, if_statement):
var endif = register_label('endif')
for clause in if_statement.clauses:
var end_clause = register_label('skip_clause')
if clause.expression != null:
generate_expression(node, clause.expression)
emit(Constants.ByteCode.JumpIfFalse, node, [Program.Operand.new(end_clause)])
generate_block(node, clause.statements)
emit(Constants.ByteCode.JumpTo, node, [Program.Operand.new(endif)])
if clause.expression != null:
emit(Constants.ByteCode.Label, node, [Program.Operand.new(end_clause)])
if clause.expression != null:
emit(Constants.ByteCode.Pop)
emit(Constants.ByteCode.Label, node, [Program.Operand.new(endif)])
func generate_option(node, option):
var destination = option.destination
if option.label == null or option.label.empty():
emit(Constants.ByteCode.RunNode, node, [Program.Operand.new(destination)])
else :
var line_id = '' #TODO: ADD TAG SUPPORT
var string_id = register_string(option.label, node.name, line_id, option.line_number, [])
emit(Constants.ByteCode.AddOption, node, [Program.Operand.new(string_id), Program.Operand.new(destination)])
func generate_assignment(node, assignment):
if assignment.operation == Constants.TokenType.EqualToOrAssign:
generate_expression(node, assignment.value)
else :
emit(Constants.ByteCode.PushVariable, node, [assignment.destination])
generate_expression(node, assignment.value)
match assignment.operation:
Constants.TokenType.AddAssign:
emit(
Constants.ByteCode.CallFunc,
node,
[Program.Operand.new(Constants.token_type_name(Constants.TokenType.Add))]
)
Constants.TokenType.MinusAssign:
emit(
Constants.ByteCode.CallFunc,
node,
[Program.Operand.new(Constants.token_type_name(Constants.TokenType.Minus))]
)
Constants.TokenType.MultiplyAssign:
emit(
Constants.ByteCode.CallFunc,
node,
[Program.Operand.new(Constants.token_type_name(Constants.TokenType.MultiplyAssign))]
)
Constants.TokenType.DivideAssign:
emit(
Constants.ByteCode.CallFunc,
node,
[Program.Operand.new(Constants.token_type_name(Constants.TokenType.DivideAssign))]
)
_:
printerr('Unable to generate assignment')
emit(Constants.ByteCode.StoreVariable, node, [Program.Operand.new(assignment.destination)])
emit(Constants.ByteCode.Pop, node)
func generate_expression(node, expression):
if self.assert(expression != null, 'Wrong expression (perhaps unterminated command block ">>"?)'):
return false
match expression.type:
Constants.ExpressionType.Value:
generate_value(node, expression.value)
Constants.ExpressionType.FunctionCall:
for parameter in expression.parameters:
generate_expression(node, parameter)
emit(Constants.ByteCode.PushNumber, node, [Program.Operand.new(expression.parameters.size())])
emit(Constants.ByteCode.CallFunc, node, [Program.Operand.new(expression.function)])
_:
printerr('No expression.')
func generate_value(node, value):
match value.value.type:
Constants.ValueType.Number:
emit(
Constants.ByteCode.PushNumber,
node,
[Program.Operand.new(value.value.as_number())]
)
Constants.ValueType.Str:
var id = register_string(
value.value.as_string(),
node.name,
'',
value.line_number,
[]
)
emit(
Constants.ByteCode.PushString,
node,
[Program.Operand.new(id)]
)
Constants.ValueType.Boolean:
emit(
Constants.ByteCode.PushBool,
node,
[Program.Operand.new(value.value.as_bool())]
)
Constants.ValueType.Variable:
emit(
Constants.ByteCode.PushVariable,
node,
[Program.Operand.new(value.value.variable)]
)
Constants.ValueType.Nullean:
emit(Constants.ByteCode.PushNull, node)
_:
printerr('Unrecognized valuenode type: %s' % value.value.type)