capitalised some files
This commit is contained in:
parent
0be6309941
commit
4251e63cc9
|
@ -14,7 +14,7 @@ __meta__ = {
|
|||
|
||||
[node name="Wol" type="Node" parent="Dialogue"]
|
||||
script = ExtResource( 1 )
|
||||
path = "res://dialogue.yarn"
|
||||
path = "res://dialogue.wol"
|
||||
auto_start = true
|
||||
variable_storage = {
|
||||
}
|
||||
|
|
214
addons/Wol/core/Constants.gd
Normal file
214
addons/Wol/core/Constants.gd
Normal file
|
@ -0,0 +1,214 @@
|
|||
extends Object
|
||||
|
||||
enum ExecutionState {
|
||||
Stopped,
|
||||
Running,
|
||||
WaitingForOption,
|
||||
Suspended
|
||||
}
|
||||
|
||||
enum HandlerState {
|
||||
PauseExecution,
|
||||
ContinueExecution
|
||||
}
|
||||
|
||||
#Compile Status return
|
||||
enum CompileStatus {
|
||||
Succeeded, SucceededUntaggedStrings,
|
||||
}
|
||||
|
||||
enum ByteCode {
|
||||
# opA = string: label name
|
||||
Label,
|
||||
# opA = string: label name
|
||||
JumpTo,
|
||||
# peek string from stack and jump to that label
|
||||
Jump,
|
||||
# opA = int: string number
|
||||
RunLine,
|
||||
# opA = string: command text
|
||||
RunCommand,
|
||||
# opA = int: string number for option to add
|
||||
AddOption,
|
||||
# present the current list of options, then clear the list; most recently
|
||||
# selected option will be on the top of the stack
|
||||
ShowOptions,
|
||||
# opA = int: string number in table; push string to stack
|
||||
PushString,
|
||||
# opA = float: number to push to stack
|
||||
PushNumber,
|
||||
# opA = int (0 or 1): bool to push to stack
|
||||
PushBool,
|
||||
# pushes a null value onto the stack
|
||||
PushNull,
|
||||
# opA = string: label name if top of stack is not null, zero or false, jumps
|
||||
# to that label
|
||||
JumpIfFalse,
|
||||
# discard top of stack
|
||||
Pop,
|
||||
# opA = string; looks up function, pops as many arguments as needed, result is
|
||||
# pushed to stack
|
||||
CallFunc,
|
||||
# opA = name of variable to get value of and push to stack
|
||||
PushVariable,
|
||||
# opA = name of variable to store top of stack in
|
||||
StoreVariable,
|
||||
# stops execution
|
||||
Stop,
|
||||
# run the node whose name is at the top of the stack
|
||||
RunNode
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
|
||||
#0 Special tokens
|
||||
Whitespace, Indent, Dedent, EndOfLine, EndOfInput,
|
||||
|
||||
#5 Numbers. Everybody loves a number
|
||||
Number,
|
||||
|
||||
#6 Strings. Everybody also loves a string
|
||||
Str,
|
||||
|
||||
#7 '#'
|
||||
TagMarker,
|
||||
|
||||
#8 Command syntax ('<<foo>>')
|
||||
BeginCommand, EndCommand,
|
||||
|
||||
ExpressionFunctionStart, # {
|
||||
ExpressionFunctionEnd, # }
|
||||
|
||||
FormatFunctionStart, # [
|
||||
FormatFunctionEnd, # ]
|
||||
|
||||
#10 Variables ('$foo')
|
||||
Variable,
|
||||
|
||||
#11 Shortcut syntax ('->')
|
||||
ShortcutOption,
|
||||
|
||||
#12 Option syntax ('[[Let's go here|Destination]]')
|
||||
OptionStart, # [[
|
||||
OptionDelimit, # |
|
||||
OptionEnd, # ]]
|
||||
|
||||
#15 Command types (specially recognised command word)
|
||||
IfToken, ElseIf, ElseToken, EndIf, Set,
|
||||
|
||||
#20 Boolean values
|
||||
TrueToken, FalseToken,
|
||||
|
||||
#22 The null value
|
||||
NullToken,
|
||||
|
||||
#23 Parentheses
|
||||
LeftParen, RightParen,
|
||||
|
||||
#25 Parameter delimiters
|
||||
Comma,
|
||||
|
||||
#26 Operators
|
||||
EqualTo, # ==, eq, is
|
||||
GreaterThan, # >, gt
|
||||
GreaterThanOrEqualTo, # >=, gte
|
||||
LessThan, # <, lt
|
||||
LessThanOrEqualTo, # <=, lte
|
||||
NotEqualTo, # !=, neq
|
||||
|
||||
#32 Logical operators
|
||||
Or, # ||, or
|
||||
And, # &&, and
|
||||
Xor, # ^, xor
|
||||
Not, # !, not
|
||||
|
||||
# this guy's special because '=' can mean either 'equal to'
|
||||
#36 or 'becomes' depending on context
|
||||
EqualToOrAssign, # =, to
|
||||
|
||||
#37
|
||||
UnaryMinus, # -; this is differentiated from Minus
|
||||
# when parsing expressions
|
||||
|
||||
#38
|
||||
Add, # +
|
||||
Minus, # -
|
||||
Multiply, # *
|
||||
Divide, # /
|
||||
Modulo, # %
|
||||
|
||||
#43
|
||||
AddAssign, # +=
|
||||
MinusAssign, # -=
|
||||
MultiplyAssign, # *=
|
||||
DivideAssign, # /=
|
||||
|
||||
Comment, # a run of text that we ignore
|
||||
|
||||
Identifier, # a single word (used for functions)
|
||||
|
||||
Text # a run of text until we hit other syntax
|
||||
}
|
||||
|
||||
enum ExpressionType {
|
||||
Value,
|
||||
FunctionCall
|
||||
}
|
||||
|
||||
enum StatementTypes {
|
||||
CustomCommand,
|
||||
ShortcutOptionGroup,
|
||||
Block,
|
||||
IfStatement,
|
||||
OptionStatement,
|
||||
AssignmentStatement,
|
||||
Line,
|
||||
InlineExpression
|
||||
}
|
||||
|
||||
enum ValueType {
|
||||
Number,
|
||||
Str,
|
||||
Boolean,
|
||||
Variable,
|
||||
Nullean
|
||||
}
|
||||
|
||||
static func token_type_name(value):
|
||||
for key in TokenType.keys():
|
||||
if TokenType[key] == value:
|
||||
return key
|
||||
return 'NOTVALID'
|
||||
|
||||
static func merge_dir(target, patch):
|
||||
for key in patch:
|
||||
target[key] = patch[key]
|
||||
|
||||
static func bytecode_name(bytecode):
|
||||
return [
|
||||
'Label',
|
||||
'JumpTo',
|
||||
'Jump',
|
||||
'RunLine',
|
||||
'RunCommand',
|
||||
'AddOption',
|
||||
'ShowOptions',
|
||||
'PushString',
|
||||
'PushNumber',
|
||||
'PushBool',
|
||||
'PushNull',
|
||||
'JumpIfFalse',
|
||||
'Pop',
|
||||
'CallFunc',
|
||||
'PushVariable',
|
||||
'StoreVariable',
|
||||
'Stop',
|
||||
'RunNode'
|
||||
][bytecode]
|
||||
|
||||
static func token_name(type):
|
||||
for key in TokenType.keys():
|
||||
if TokenType[key] == type:
|
||||
return key
|
||||
return ''
|
||||
|
25
addons/Wol/core/Library.gd
Normal file
25
addons/Wol/core/Library.gd
Normal file
|
@ -0,0 +1,25 @@
|
|||
extends Object
|
||||
|
||||
const FunctionInfo = preload('res://addons/Wol/core/FunctionInfo.gd')
|
||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||
|
||||
var functions = {}
|
||||
var virtual_machine
|
||||
|
||||
func get_function(name):
|
||||
if functions.has(name):
|
||||
return functions[name]
|
||||
else :
|
||||
printerr('Invalid Function: %s'% name)
|
||||
return
|
||||
|
||||
func import_library(other):
|
||||
Constants.merge_dir(functions, other.functions)
|
||||
other.virtual_machine = virtual_machine
|
||||
|
||||
func register_function(name, parameter_count, function, returns_value):
|
||||
var functionInfo = FunctionInfo.new(name, parameter_count, function, returns_value)
|
||||
functions[name] = functionInfo
|
||||
|
||||
func deregister_function(name):
|
||||
functions.erase(name)
|
146
addons/Wol/core/Program.gd
Normal file
146
addons/Wol/core/Program.gd
Normal file
|
@ -0,0 +1,146 @@
|
|||
extends Object
|
||||
|
||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||
|
||||
var name = ''
|
||||
var filename = ''
|
||||
var strings = {}
|
||||
var nodes = {}
|
||||
|
||||
class Line:
|
||||
var text = ''
|
||||
var node_name = ''
|
||||
var line_number = -1
|
||||
var file_name = ''
|
||||
var implicit = false
|
||||
var substitutions = []
|
||||
var meta = []
|
||||
|
||||
func _init(_text, _node_name, _line_number, _file_name, _implicit, _meta):
|
||||
text = _text
|
||||
node_name = _node_name
|
||||
file_name = _file_name
|
||||
implicit = _implicit
|
||||
meta = _meta
|
||||
|
||||
func clone():
|
||||
return get_script().new(text, node_name, line_number, file_name, implicit, meta)
|
||||
|
||||
func _to_string():
|
||||
return '%s:%d: "%s"' % [file_name.get_file(), line_number, text]
|
||||
|
||||
class Option:
|
||||
var line
|
||||
var id = -1
|
||||
var destination = ''
|
||||
|
||||
func _init(_line, _id, _destination):
|
||||
line = _line
|
||||
id = _id
|
||||
destination = _destination
|
||||
|
||||
func clone():
|
||||
return get_script().new(self)
|
||||
|
||||
class Command:
|
||||
var command = ''
|
||||
|
||||
func _init(_command):
|
||||
command = _command
|
||||
|
||||
class WolNode:
|
||||
var name = ''
|
||||
var instructions = []
|
||||
var labels = {}
|
||||
var tags = []
|
||||
var source_id = ''
|
||||
|
||||
func _init(other = null):
|
||||
if other != null and other.get_script() == self.get_script():
|
||||
name = other.name
|
||||
instructions += other.instructions
|
||||
for key in other.labels.keys():
|
||||
labels[key] = other.labels[key]
|
||||
tags += other.tags
|
||||
source_id = other.source_id
|
||||
|
||||
func equals(other):
|
||||
if other.get_script() != get_script():
|
||||
return false
|
||||
if other.name != name:
|
||||
return false
|
||||
if other.instructions != instructions:
|
||||
return false
|
||||
if other.labels != labels:
|
||||
return false
|
||||
if other.source_id != source_id:
|
||||
return false
|
||||
return true
|
||||
|
||||
func _to_string():
|
||||
return "WolNode[%s:%s]" % [name, source_id]
|
||||
|
||||
# TODO: Make this make sense
|
||||
class Operand:
|
||||
enum ValueType {
|
||||
None,
|
||||
StringValue,
|
||||
BooleanValue,
|
||||
FloatValue
|
||||
}
|
||||
|
||||
var value
|
||||
var type
|
||||
|
||||
func _init(_value):
|
||||
if typeof(_value) == TYPE_OBJECT and _value.get_script() == get_script():
|
||||
set_value(_value.value)
|
||||
else:
|
||||
set_value(_value)
|
||||
|
||||
func set_value(_value):
|
||||
match typeof(_value):
|
||||
TYPE_REAL,TYPE_INT:
|
||||
set_number(_value)
|
||||
TYPE_BOOL:
|
||||
set_boolean(_value)
|
||||
TYPE_STRING:
|
||||
set_string(_value)
|
||||
|
||||
func set_boolean(_value):
|
||||
value = _value
|
||||
type = ValueType.BooleanValue
|
||||
return self
|
||||
|
||||
func set_string(_value):
|
||||
value = _value
|
||||
type = ValueType.StringValue
|
||||
return self
|
||||
|
||||
func set_number(_value):
|
||||
value = _value
|
||||
type = ValueType.FloatValue
|
||||
return self
|
||||
|
||||
func clear_value():
|
||||
type = ValueType.None
|
||||
value = null
|
||||
|
||||
func clone():
|
||||
return get_script().new(self)
|
||||
|
||||
func _to_string():
|
||||
return "Operand[%s:%s]" % [type, value]
|
||||
|
||||
class Instruction:
|
||||
var operation = -1
|
||||
var operands = []
|
||||
|
||||
func _init(other = null):
|
||||
if other != null and other.get_script() == self.get_script():
|
||||
self.operation = other.operation
|
||||
self.operands += other.operands
|
||||
|
||||
func _to_string():
|
||||
return Constants.bytecode_name(operation) + ':' + operands as String
|
||||
|
137
addons/Wol/core/Value.gd
Normal file
137
addons/Wol/core/Value.gd
Normal file
|
@ -0,0 +1,137 @@
|
|||
extends Object
|
||||
|
||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||
|
||||
const NANI = 'NaN'
|
||||
|
||||
var type = Constants.ValueType.Nullean
|
||||
var number = 0
|
||||
var string = ''
|
||||
var variable = ''
|
||||
var boolean = false
|
||||
|
||||
func _init(value = NANI):
|
||||
if typeof(value) == TYPE_OBJECT and value.get_script() == get_script():
|
||||
if value.type == Constants.ValueType.Variable:
|
||||
type = value.type
|
||||
variable = value.variable
|
||||
else:
|
||||
set_value(value)
|
||||
|
||||
func value():
|
||||
match type:
|
||||
Constants.ValueType.Number:
|
||||
return number
|
||||
Constants.ValueType.Str:
|
||||
return string
|
||||
Constants.ValueType.Boolean:
|
||||
return boolean
|
||||
Constants.ValueType.Variable:
|
||||
return variable
|
||||
return null
|
||||
|
||||
func as_bool():
|
||||
match type:
|
||||
Constants.ValueType.Number:
|
||||
return number != 0
|
||||
Constants.ValueType.Str:
|
||||
return !string.empty()
|
||||
Constants.ValueType.Boolean:
|
||||
return boolean
|
||||
return false
|
||||
|
||||
func as_string():
|
||||
return '%s' % value()
|
||||
|
||||
func as_number():
|
||||
match type:
|
||||
Constants.ValueType.Number:
|
||||
return number
|
||||
Constants.ValueType.Str:
|
||||
return float(string)
|
||||
Constants.ValueType.Boolean:
|
||||
return 0.0 if !boolean else 1.0
|
||||
return .0
|
||||
|
||||
func set_value(value):
|
||||
if value == null or (typeof(value) == TYPE_STRING and value == NANI):
|
||||
type = Constants.ValueType.Nullean
|
||||
return
|
||||
|
||||
match typeof(value):
|
||||
TYPE_INT, TYPE_REAL:
|
||||
type = Constants.ValueType.Number
|
||||
number = value
|
||||
TYPE_STRING:
|
||||
type = Constants.ValueType.Str
|
||||
string = value
|
||||
TYPE_BOOL:
|
||||
type = Constants.ValueType.Boolean
|
||||
boolean = value
|
||||
|
||||
func add(other):
|
||||
if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str:
|
||||
return get_script().new('%s%s' % [value(), other.value()])
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return get_script().new(number + other.number)
|
||||
return null
|
||||
|
||||
func equals(other):
|
||||
if other.get_script() != get_script():
|
||||
return false
|
||||
if other.value() != value():
|
||||
return false
|
||||
# TODO: Add more equality cases
|
||||
return true
|
||||
|
||||
func sub(other):
|
||||
if type == Constants.ValueType.Str or other.type == Constants.ValueType.Str:
|
||||
return get_script().new(str(value()).replace(str(other.value()),''))
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return get_script().new(number - other.number)
|
||||
return null
|
||||
|
||||
func mult(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return get_script().new(number * other.number)
|
||||
return null
|
||||
|
||||
func div(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return get_script().new(number / other.number)
|
||||
return null
|
||||
|
||||
func mod(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return get_script().new(number % other.number)
|
||||
return null
|
||||
|
||||
func negative():
|
||||
if type == Constants.ValueType.Number:
|
||||
return get_script().new(-number)
|
||||
return null
|
||||
|
||||
func greater(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return number > other.number
|
||||
return false
|
||||
|
||||
func less(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return number < other.number
|
||||
return false
|
||||
|
||||
func geq(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return number > other.number or equals(other)
|
||||
return false
|
||||
|
||||
func leq(other):
|
||||
if type == Constants.ValueType.Number and other.type == Constants.ValueType.Number:
|
||||
return number < other.number or equals(other)
|
||||
return false
|
||||
|
||||
func _to_string():
|
||||
return 'value(type[%s]: %s)' % [type,value()]
|
||||
|
||||
|
461
addons/Wol/core/compiler/Compiler.gd
Normal file
461
addons/Wol/core/compiler/Compiler.gd
Normal file
|
@ -0,0 +1,461 @@
|
|||
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:
|
||||
self.source = _source
|
||||
else:
|
||||
var file = File.new()
|
||||
file.open(_filename, File.READ)
|
||||
self.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, message + ('; on line %d column %d' % [line_number, column]))
|
||||
elif not statement:
|
||||
emit_signal('error', message, line_number, column)
|
||||
|
||||
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):
|
||||
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)
|
485
addons/Wol/core/compiler/Lexer.gd
Normal file
485
addons/Wol/core/compiler/Lexer.gd
Normal file
|
@ -0,0 +1,485 @@
|
|||
extends Object
|
||||
|
||||
const Constants = preload('res://addons/Wol/core/Constants.gd')
|
||||
|
||||
const LINE_COMMENT = '//'
|
||||
const FORWARD_SLASH = '/'
|
||||
const LINE_SEPARATOR = '\n'
|
||||
|
||||
const BASE = 'base'
|
||||
const DASH = '-'
|
||||
const COMMAND = 'command'
|
||||
const LINK = 'link'
|
||||
const SHORTCUT = 'shortcut'
|
||||
const TAG = 'tag'
|
||||
const EXPRESSION = 'expression'
|
||||
const ASSIGNMENT = 'assignment'
|
||||
const OPTION = 'option'
|
||||
const OR = 'or'
|
||||
const DESTINATION = 'destination'
|
||||
const INLINE = 'inline'
|
||||
const FORMAT_FUNCTION = 'format'
|
||||
|
||||
var WHITESPACE = '\\s*'
|
||||
|
||||
var compiler
|
||||
var filename = ''
|
||||
var title = ''
|
||||
var text = ''
|
||||
|
||||
var states = {}
|
||||
var default_state
|
||||
var current_state
|
||||
|
||||
var indent_stack = []
|
||||
var should_track_indent = false
|
||||
|
||||
func _init(_compiler, _filename, _title, _text):
|
||||
create_states()
|
||||
|
||||
compiler = _compiler
|
||||
filename = _filename
|
||||
title = _title
|
||||
text = _text
|
||||
|
||||
func create_states():
|
||||
var patterns = {}
|
||||
patterns[Constants.TokenType.Text] = ['.*', 'any text']
|
||||
|
||||
patterns[Constants.TokenType.Number] = ['\\-?[0-9]+(\\.[0-9+])?', 'any number']
|
||||
patterns[Constants.TokenType.Str] = ['\"([^\"\\\\]*(?:\\.[^\"\\\\]*)*)\"', 'any text']
|
||||
patterns[Constants.TokenType.TagMarker] = ['\\#', 'a tag #']
|
||||
patterns[Constants.TokenType.LeftParen] = ['\\(', 'left parenthesis (']
|
||||
patterns[Constants.TokenType.RightParen] = ['\\)', 'right parenthesis )']
|
||||
patterns[Constants.TokenType.EqualTo] = ['(==|is(?!\\w)|eq(?!\\w))', '"=", "is" or "eq"']
|
||||
patterns[Constants.TokenType.EqualToOrAssign] = ['(=|to(?!\\w))', '"=" or "to"']
|
||||
patterns[Constants.TokenType.NotEqualTo] = ['(\\!=|neq(?!\\w))', '"!=" or "neq"']
|
||||
patterns[Constants.TokenType.GreaterThanOrEqualTo] = ['(\\>=|gte(?!\\w))', '">=" or "gte"']
|
||||
patterns[Constants.TokenType.GreaterThan] = ['(\\>|gt(?!\\w))', '">" or "gt"']
|
||||
patterns[Constants.TokenType.LessThanOrEqualTo] = ['(\\<=|lte(?!\\w))', '"<=" or "lte"']
|
||||
patterns[Constants.TokenType.LessThan] = ['(\\<|lt(?!\\w))', '"<" or "lt"']
|
||||
patterns[Constants.TokenType.AddAssign] = ['\\+=', '"+="']
|
||||
patterns[Constants.TokenType.MinusAssign] = ['\\-=', '"-="']
|
||||
patterns[Constants.TokenType.MultiplyAssign] = ['\\*=', '"*="']
|
||||
patterns[Constants.TokenType.DivideAssign] = ['\\/=', '"/="']
|
||||
patterns[Constants.TokenType.Add] = ['\\+', '"+"']
|
||||
patterns[Constants.TokenType.Minus] = ['\\-', '"-"']
|
||||
patterns[Constants.TokenType.Multiply] = ['\\*', '"*"']
|
||||
patterns[Constants.TokenType.Divide] = ['\\/', '"/"']
|
||||
patterns[Constants.TokenType.Modulo] = ['\\%', '"%"']
|
||||
patterns[Constants.TokenType.And] = ['(\\&\\&|and(?!\\w))', '"&&" or "and"']
|
||||
patterns[Constants.TokenType.Or] = ['(\\|\\||or(?!\\w))', '"||" or "or"']
|
||||
patterns[Constants.TokenType.Xor] = ['(\\^|xor(?!\\w))', '"^" or "xor"']
|
||||
patterns[Constants.TokenType.Not] = ['(\\!|not(?!\\w))', '"!" or "not"']
|
||||
patterns[Constants.TokenType.Variable] = ['\\$([A-Za-z0-9_\\.])+', 'any variable']
|
||||
patterns[Constants.TokenType.Comma] = ['\\,', '","']
|
||||
patterns[Constants.TokenType.TrueToken] = ['true(?!\\w)', '"true"']
|
||||
patterns[Constants.TokenType.FalseToken] = ['false(?!\\w)', '"false"']
|
||||
patterns[Constants.TokenType.NullToken] = ['null(?!\\w)', '"null"']
|
||||
patterns[Constants.TokenType.BeginCommand] = ['\\<\\<', 'beginning of a command "<<"']
|
||||
patterns[Constants.TokenType.EndCommand] = ['\\>\\>', 'ending of a command ">>"']
|
||||
patterns[Constants.TokenType.OptionStart] = ['\\[\\[', 'start of an option "[["']
|
||||
patterns[Constants.TokenType.OptionEnd] = ['\\]\\]', 'end of an option "]]"']
|
||||
patterns[Constants.TokenType.OptionDelimit] = ['\\|', 'middle of an option "|"']
|
||||
patterns[Constants.TokenType.Identifier] = ['[a-zA-Z0-9_:\\.]+', 'any reference to another node']
|
||||
patterns[Constants.TokenType.IfToken] = ['if(?!\\w)', '"if"']
|
||||
patterns[Constants.TokenType.ElseToken] = ['else(?!\\w)', '"else"']
|
||||
patterns[Constants.TokenType.ElseIf] = ['elseif(?!\\w)', '"elseif"']
|
||||
patterns[Constants.TokenType.EndIf] = ['endif(?!\\w)', '"endif"']
|
||||
patterns[Constants.TokenType.Set] = ['set(?!\\w)', '"set"']
|
||||
patterns[Constants.TokenType.ShortcutOption] = ['\\-\\>\\s*', '"->"']
|
||||
patterns[Constants.TokenType.ExpressionFunctionStart] = ['\\{', '"{"']
|
||||
patterns[Constants.TokenType.ExpressionFunctionEnd] = ['\\}', '"}"']
|
||||
patterns[Constants.TokenType.FormatFunctionStart] = ['(?<!\\[)\\[(?!\\[)', '"["']
|
||||
patterns[Constants.TokenType.FormatFunctionEnd] = ['\\]', '"]"']
|
||||
|
||||
var shortcut_option = SHORTCUT + DASH + OPTION
|
||||
var shortcut_option_tag = shortcut_option + DASH + TAG
|
||||
var command_or_expression = COMMAND + DASH + OR + DASH + EXPRESSION
|
||||
var link_destination = LINK + DASH + DESTINATION
|
||||
var format_expression = FORMAT_FUNCTION + DASH + EXPRESSION
|
||||
var inline_expression = INLINE + DASH + EXPRESSION
|
||||
var link_inline_expression = LINK + DASH + INLINE + DASH + EXPRESSION
|
||||
var link_format_expression = LINK + DASH + FORMAT_FUNCTION + DASH + EXPRESSION
|
||||
|
||||
states = {}
|
||||
|
||||
states[BASE] = LexerState.new(patterns)
|
||||
states[BASE].add_transition(Constants.TokenType.BeginCommand, COMMAND, true)
|
||||
states[BASE].add_transition(Constants.TokenType.ExpressionFunctionStart, inline_expression, true)
|
||||
states[BASE].add_transition(Constants.TokenType.FormatFunctionStart, FORMAT_FUNCTION, true)
|
||||
states[BASE].add_transition(Constants.TokenType.OptionStart, LINK, true)
|
||||
states[BASE].add_transition(Constants.TokenType.ShortcutOption, shortcut_option)
|
||||
states[BASE].add_transition(Constants.TokenType.TagMarker, TAG, true)
|
||||
states[BASE].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
#FIXME - Tags are not being proccessed properly this way. We must look for the format #{identifier}:{value}
|
||||
# Possible solution is to add more transitions
|
||||
states[TAG] = LexerState.new(patterns)
|
||||
states[TAG].add_transition(Constants.TokenType.Identifier, BASE)
|
||||
|
||||
states[shortcut_option] = LexerState.new(patterns)
|
||||
states[shortcut_option].track_indent = true
|
||||
states[shortcut_option].add_transition(Constants.TokenType.BeginCommand, EXPRESSION, true)
|
||||
states[shortcut_option].add_transition(Constants.TokenType.ExpressionFunctionStart, inline_expression, true)
|
||||
states[shortcut_option].add_transition(Constants.TokenType.TagMarker, shortcut_option_tag, true)
|
||||
states[shortcut_option].add_text_rule(Constants.TokenType.Text, BASE)
|
||||
|
||||
states[shortcut_option_tag] = LexerState.new(patterns)
|
||||
states[shortcut_option_tag].add_transition(Constants.TokenType.Identifier, shortcut_option)
|
||||
|
||||
states[COMMAND] = LexerState.new(patterns)
|
||||
states[COMMAND].add_transition(Constants.TokenType.IfToken, EXPRESSION)
|
||||
states[COMMAND].add_transition(Constants.TokenType.ElseToken)
|
||||
states[COMMAND].add_transition(Constants.TokenType.ElseIf, EXPRESSION)
|
||||
states[COMMAND].add_transition(Constants.TokenType.EndIf)
|
||||
states[COMMAND].add_transition(Constants.TokenType.Set, ASSIGNMENT)
|
||||
states[COMMAND].add_transition(Constants.TokenType.EndCommand, BASE, true)
|
||||
states[COMMAND].add_transition(Constants.TokenType.Identifier, command_or_expression)
|
||||
states[COMMAND].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
states[command_or_expression] = LexerState.new(patterns)
|
||||
states[command_or_expression].add_transition(Constants.TokenType.LeftParen, EXPRESSION)
|
||||
states[command_or_expression].add_transition(Constants.TokenType.EndCommand, BASE, true)
|
||||
states[command_or_expression].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
states[ASSIGNMENT] = LexerState.new(patterns)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.Variable)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.EqualToOrAssign, EXPRESSION)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.AddAssign, EXPRESSION)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.MinusAssign, EXPRESSION)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.MultiplyAssign, EXPRESSION)
|
||||
states[ASSIGNMENT].add_transition(Constants.TokenType.DivideAssign, EXPRESSION)
|
||||
|
||||
states[FORMAT_FUNCTION] = LexerState.new(patterns)
|
||||
states[FORMAT_FUNCTION].add_transition(Constants.TokenType.FormatFunctionEnd, BASE, true)
|
||||
states[FORMAT_FUNCTION].add_transition(Constants.TokenType.ExpressionFunctionStart, format_expression, true)
|
||||
states[FORMAT_FUNCTION].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
|
||||
states[format_expression] = LexerState.new(patterns)
|
||||
states[format_expression].add_transition(Constants.TokenType.ExpressionFunctionEnd, FORMAT_FUNCTION)
|
||||
form_expression_state(states[format_expression])
|
||||
|
||||
states[inline_expression] = LexerState.new(patterns)
|
||||
states[inline_expression].add_transition(Constants.TokenType.ExpressionFunctionEnd, BASE)
|
||||
form_expression_state(states[inline_expression])
|
||||
|
||||
states[EXPRESSION] = LexerState.new(patterns)
|
||||
states[EXPRESSION].add_transition(Constants.TokenType.EndCommand, BASE)
|
||||
# states[EXPRESSION].add_transition(Constants.TokenType.FormatFunctionEnd, BASE)
|
||||
form_expression_state(states[EXPRESSION])
|
||||
|
||||
states[LINK] = LexerState.new(patterns)
|
||||
states[LINK].add_transition(Constants.TokenType.OptionEnd, BASE, true)
|
||||
states[LINK].add_transition(Constants.TokenType.ExpressionFunctionStart, link_inline_expression, true)
|
||||
states[LINK].add_transition(Constants.TokenType.FormatFunctionStart, link_format_expression, true)
|
||||
states[LINK].add_transition(Constants.TokenType.FormatFunctionEnd, LINK, true)
|
||||
states[LINK].add_transition(Constants.TokenType.OptionDelimit, link_destination, true)
|
||||
states[LINK].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
states[link_format_expression] = LexerState.new(patterns)
|
||||
states[link_format_expression].add_transition(Constants.TokenType.FormatFunctionEnd, LINK, true)
|
||||
states[link_format_expression].add_transition(Constants.TokenType.ExpressionFunctionStart, link_inline_expression, true)
|
||||
states[link_format_expression].add_text_rule(Constants.TokenType.Text)
|
||||
|
||||
states[link_inline_expression] = LexerState.new(patterns)
|
||||
states[link_inline_expression].add_transition(Constants.TokenType.ExpressionFunctionEnd, LINK)
|
||||
form_expression_state(states[link_inline_expression])
|
||||
|
||||
states[link_destination] = LexerState.new(patterns)
|
||||
states[link_destination].add_transition(Constants.TokenType.Identifier)
|
||||
states[link_destination].add_transition(Constants.TokenType.OptionEnd, BASE)
|
||||
|
||||
default_state = states[BASE]
|
||||
|
||||
for key in states.keys():
|
||||
states[key].name = key
|
||||
|
||||
func form_expression_state(expression_state):
|
||||
expression_state.add_transition(Constants.TokenType.Number)
|
||||
expression_state.add_transition(Constants.TokenType.Str)
|
||||
expression_state.add_transition(Constants.TokenType.LeftParen)
|
||||
expression_state.add_transition(Constants.TokenType.RightParen)
|
||||
expression_state.add_transition(Constants.TokenType.EqualTo)
|
||||
expression_state.add_transition(Constants.TokenType.EqualToOrAssign)
|
||||
expression_state.add_transition(Constants.TokenType.NotEqualTo)
|
||||
expression_state.add_transition(Constants.TokenType.GreaterThanOrEqualTo)
|
||||
expression_state.add_transition(Constants.TokenType.GreaterThan)
|
||||
expression_state.add_transition(Constants.TokenType.LessThanOrEqualTo)
|
||||
expression_state.add_transition(Constants.TokenType.LessThan)
|
||||
expression_state.add_transition(Constants.TokenType.Add)
|
||||
expression_state.add_transition(Constants.TokenType.Minus)
|
||||
expression_state.add_transition(Constants.TokenType.Multiply)
|
||||
expression_state.add_transition(Constants.TokenType.Divide)
|
||||
expression_state.add_transition(Constants.TokenType.Modulo)
|
||||
expression_state.add_transition(Constants.TokenType.And)
|
||||
expression_state.add_transition(Constants.TokenType.Or)
|
||||
expression_state.add_transition(Constants.TokenType.Xor)
|
||||
expression_state.add_transition(Constants.TokenType.Not)
|
||||
expression_state.add_transition(Constants.TokenType.Variable)
|
||||
expression_state.add_transition(Constants.TokenType.Comma)
|
||||
expression_state.add_transition(Constants.TokenType.TrueToken)
|
||||
expression_state.add_transition(Constants.TokenType.FalseToken)
|
||||
expression_state.add_transition(Constants.TokenType.NullToken)
|
||||
expression_state.add_transition(Constants.TokenType.Identifier)
|
||||
|
||||
func tokenize():
|
||||
var tokens = []
|
||||
|
||||
indent_stack.clear()
|
||||
indent_stack.push_front([0, false])
|
||||
should_track_indent = false
|
||||
current_state = default_state
|
||||
|
||||
var lines = text.split(LINE_SEPARATOR)
|
||||
var line_number = 1
|
||||
|
||||
lines.append('')
|
||||
|
||||
for line in lines:
|
||||
var line_tokens = tokenize_line(line, line_number)
|
||||
if line_tokens == null:
|
||||
return
|
||||
|
||||
tokens.append_array(line_tokens)
|
||||
line_number += 1
|
||||
|
||||
var end_of_input = Token.new(
|
||||
Constants.TokenType.EndOfInput,
|
||||
current_state,
|
||||
line_number,
|
||||
0
|
||||
)
|
||||
tokens.append(end_of_input)
|
||||
|
||||
return tokens
|
||||
|
||||
func tokenize_line(line, line_number):
|
||||
var token_stack = []
|
||||
|
||||
var fresh_line = line.replace('\t', ' ').replace('\r', '')
|
||||
|
||||
var indentation = line_indentation(line)
|
||||
var previous_indentation = indent_stack.front()[0]
|
||||
|
||||
if should_track_indent and indentation > previous_indentation:
|
||||
indent_stack.push_front([indentation, true])
|
||||
|
||||
var indent = Token.new(
|
||||
Constants.TokenType.Indent,
|
||||
current_state,
|
||||
filename,
|
||||
line_number,
|
||||
previous_indentation
|
||||
)
|
||||
indent.value = '%*s' % [indentation - previous_indentation, '']
|
||||
|
||||
should_track_indent = false
|
||||
token_stack.push_front(indent)
|
||||
|
||||
elif indentation < previous_indentation:
|
||||
while indentation < indent_stack.front()[0]:
|
||||
var top = indent_stack.pop_front()[1]
|
||||
if top:
|
||||
var deindent = Token.new(Constants.TokenType.Dedent, current_state, line_number, 0)
|
||||
token_stack.push_front(deindent)
|
||||
|
||||
var column = indentation
|
||||
var whitespace = RegEx.new()
|
||||
whitespace.compile(WHITESPACE)
|
||||
|
||||
while column < fresh_line.length():
|
||||
if fresh_line.substr(column).begins_with(LINE_COMMENT):
|
||||
break
|
||||
|
||||
var matched = false
|
||||
|
||||
for rule in current_state.rules:
|
||||
var found = rule.regex.search(fresh_line, column)
|
||||
|
||||
if !found:
|
||||
continue
|
||||
|
||||
var token_text = ''
|
||||
|
||||
# NOTE: If this is text then we back up to the most recent delimiting token
|
||||
# and treat everything from there as text.
|
||||
if rule.token_type == Constants.TokenType.Text:
|
||||
|
||||
var start_index = indentation
|
||||
|
||||
if token_stack.size() > 0 :
|
||||
while token_stack.front().type == Constants.TokenType.Identifier:
|
||||
token_stack.pop_front()
|
||||
|
||||
var start_delimit_token = token_stack.front()
|
||||
start_index = start_delimit_token.column
|
||||
|
||||
if start_delimit_token.type == Constants.TokenType.Indent:
|
||||
start_index += start_delimit_token.value.length()
|
||||
if start_delimit_token.type == Constants.TokenType.Dedent:
|
||||
start_index = indentation
|
||||
|
||||
column = start_index
|
||||
var end_index = found.get_start() + found.get_string().length()
|
||||
|
||||
token_text = fresh_line.substr(start_index, end_index - start_index)
|
||||
else:
|
||||
token_text = found.get_string()
|
||||
|
||||
column += token_text.length()
|
||||
|
||||
if rule.token_type == Constants.TokenType.Str:
|
||||
token_text = token_text.substr(1, token_text.length() - 2)
|
||||
token_text = token_text.replace('\\\\', '\\')
|
||||
token_text = token_text.replace('\\\'','\'')
|
||||
|
||||
var token = Token.new(
|
||||
rule.token_type,
|
||||
current_state,
|
||||
filename,
|
||||
line_number,
|
||||
column,
|
||||
token_text
|
||||
)
|
||||
token.delimits_text = rule.delimits_text
|
||||
|
||||
token_stack.push_front(token)
|
||||
|
||||
if rule.enter_state != null and rule.enter_state.length() > 0:
|
||||
if not states.has(rule.enter_state):
|
||||
printerr('State[%s] not known - line(%s) col(%s)' % [rule.enter_state, line_number, column])
|
||||
return []
|
||||
|
||||
enter_state(states[rule.enter_state])
|
||||
|
||||
if should_track_indent:
|
||||
if indent_stack.front()[0] < indentation:
|
||||
indent_stack.append([indentation, false])
|
||||
|
||||
matched = true
|
||||
break
|
||||
|
||||
if not matched:
|
||||
var rules = []
|
||||
for rule in current_state.rules:
|
||||
rules.append('"%s" (%s)' % [Constants.token_type_name(rule.token_type), rule.human_readable_identifier])
|
||||
|
||||
var error_data = [
|
||||
PoolStringArray(rules).join(', ') if rules.size() == 1 else PoolStringArray(rules.slice(0, rules.size() - 2)).join(', ') + ' or %s' % rules[-1],
|
||||
filename,
|
||||
title,
|
||||
line_number,
|
||||
column
|
||||
]
|
||||
compiler.assert(false, 'Expected %s in file %s in node "%s" on line #%d (column #%d)' % error_data, line_number, column)
|
||||
return
|
||||
|
||||
var last_whitespace = whitespace.search(line, column)
|
||||
if last_whitespace:
|
||||
column += last_whitespace.get_string().length()
|
||||
|
||||
token_stack.invert()
|
||||
|
||||
return token_stack
|
||||
|
||||
func line_indentation(line):
|
||||
var indent_regex = RegEx.new()
|
||||
indent_regex.compile('^(\\s*)')
|
||||
|
||||
var found = indent_regex.search(line)
|
||||
|
||||
if !found or found.get_string().length() <= 0:
|
||||
return 0
|
||||
|
||||
return found.get_string().length()
|
||||
|
||||
func enter_state(state):
|
||||
current_state = state;
|
||||
if current_state.track_indent:
|
||||
should_track_indent = true
|
||||
|
||||
class Token:
|
||||
var type = -1
|
||||
var value = ''
|
||||
|
||||
var filename = ''
|
||||
var line_number = -1
|
||||
var column = -1
|
||||
var text = ''
|
||||
|
||||
var delimits_text = false
|
||||
var parameter_count = -1
|
||||
var lexer_state = ''
|
||||
|
||||
func _init(_type, _state, _filename, _line_number = -1, _column = -1, _value = ''):
|
||||
type = _type
|
||||
lexer_state = _state.name
|
||||
filename = _filename
|
||||
line_number = _line_number
|
||||
column = _column
|
||||
value = _value
|
||||
|
||||
func _to_string():
|
||||
return '%s (%s) at %s:%s (state: %s)' % [Constants.token_type_name(type), value, line_number, column, lexer_state]
|
||||
|
||||
class LexerState:
|
||||
var name = ''
|
||||
var patterns = {}
|
||||
var rules = []
|
||||
var track_indent = false
|
||||
|
||||
func _init(_patterns):
|
||||
patterns = _patterns
|
||||
|
||||
func add_transition(type, state = '', delimit_text = false):
|
||||
var pattern = '\\G%s' % patterns[type][0]
|
||||
var rule = Rule.new(type, pattern, patterns[type][1], state, delimit_text)
|
||||
rules.append(rule)
|
||||
return rule
|
||||
|
||||
func add_text_rule(type, state = ''):
|
||||
if contains_text_rule() :
|
||||
printerr('State already contains Text rule')
|
||||
return null
|
||||
|
||||
var delimiters:Array = []
|
||||
for rule in rules:
|
||||
if rule.delimits_text:
|
||||
delimiters.append('%s' % rule.regex.get_pattern().substr(2))
|
||||
|
||||
var pattern = '\\G((?!%s).)*' % [PoolStringArray(delimiters).join('|')]
|
||||
var rule = add_transition(type, state)
|
||||
rule.regex = RegEx.new()
|
||||
rule.regex.compile(pattern)
|
||||
rule.is_text_rule = true
|
||||
return rule
|
||||
|
||||
func contains_text_rule():
|
||||
for rule in rules:
|
||||
if rule.is_text_rule:
|
||||
return true
|
||||
return false
|
||||
|
||||
class Rule:
|
||||
var regex
|
||||
|
||||
var enter_state = ''
|
||||
var token_type = -1
|
||||
var is_text_rule = false
|
||||
var delimits_text = false
|
||||
var human_readable_identifier = ''
|
||||
|
||||
func _init(_type, _regex, _human_readable_identifier, _enter_state, _delimits_text):
|
||||
token_type = _type
|
||||
|
||||
regex = RegEx.new()
|
||||
regex.compile(_regex)
|
||||
|
||||
human_readable_identifier = _human_readable_identifier
|
||||
enter_state = _enter_state
|
||||
delimits_text = _delimits_text
|
||||
|
||||
func _to_string():
|
||||
return '[Rule : %s (%s) - %s]' % [Constants.token_type_name(token_type), human_readable_identifier, regex]
|
1065
addons/Wol/core/compiler/Parser.gd
Normal file
1065
addons/Wol/core/compiler/Parser.gd
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,31 +1,3 @@
|
|||
title: Start
|
||||
tags:
|
||||
colorID:
|
||||
position: 0, 0
|
||||
---
|
||||
<<a_custom_command>>
|
||||
<<command_with multiple arguments>>
|
||||
|
||||
// remove "to" to trigger error
|
||||
<<set $direction to "that">>
|
||||
<<set $one to 1>>
|
||||
|
||||
// Implement inline expressions
|
||||
<<if visit_count() == 1>>
|
||||
Narrator: You, {$direction} way!
|
||||
<<endif>>
|
||||
Narrator: Do you know you've been here {visit_count()} times?
|
||||
You: Did you know one + one equals {$one + $one}?
|
||||
Narrator: You wanna go somewhere?
|
||||
|
||||
-> Go to the store
|
||||
[[TheStore]]
|
||||
-> How much did I visit the store?
|
||||
Narrator: You've been to the store { visit_count("TheStore") } times.
|
||||
[[Start]]
|
||||
-> Lets stay here and talk
|
||||
[[Talk]]
|
||||
===
|
||||
title: TheStore
|
||||
tags:
|
||||
colorID:
|
||||
|
@ -68,3 +40,31 @@ Narrator: Do you want to continue talking?
|
|||
[[Start]]
|
||||
-> No
|
||||
===
|
||||
title: Start
|
||||
tags:
|
||||
colorID:
|
||||
position: 0, 0
|
||||
---
|
||||
<<a_custom_command>>
|
||||
<<command_with multiple arguments>>
|
||||
|
||||
// remove "to" to trigger error
|
||||
<<set $direction to "that">>
|
||||
<<set $one to 1>>
|
||||
|
||||
// Implement inline expressions
|
||||
<<if visit_count() == 1>>
|
||||
Narrator: You, {$direction} way!
|
||||
<<endif>>
|
||||
Narrator: Do you know you've been here {visit_count()} times?
|
||||
You: Did you know one + one equals {$one + $one}?
|
||||
Narrator: You wanna go somewhere?
|
||||
|
||||
-> Go to the store
|
||||
[[TheStore]]
|
||||
-> How much did I visit the store?
|
||||
Narrator: You've been to the store { visit_count("TheStore") } times.
|
||||
[[Start]]
|
||||
-> Lets stay here and talk
|
||||
[[Talk]]
|
||||
===
|
|
@ -11,7 +11,7 @@ config_version=4
|
|||
[application]
|
||||
|
||||
config/name="Wol"
|
||||
run/main_scene="res://Dialogue.tscn"
|
||||
run/main_scene="res://TestScene.tscn"
|
||||
config/icon="res://icon.png"
|
||||
|
||||
[debug]
|
||||
|
|
Reference in a new issue