diff --git a/cli/loreline/Cli.hx b/cli/loreline/Cli.hx index db895b8..4b04dbd 100644 --- a/cli/loreline/Cli.hx +++ b/cli/loreline/Cli.hx @@ -132,12 +132,15 @@ class Cli { print(Json.stringify(script.toJson(), null, ' ')); } catch (e:Any) { + #if debug if (e is Error) { printStackTrace(false, (e:Error).stack); + error((e:Error).toString()); } else { printStackTrace(false, CallStack.exceptionStack()); } + #end fail(e, file); } @@ -170,6 +173,7 @@ class Cli { #if debug if (e is Error) { printStackTrace(false, (e:Error).stack); + error((e:Error).toString()); } else { printStackTrace(false, CallStack.exceptionStack()); diff --git a/run.n b/run.n index 59cec21..a7c3411 100644 Binary files a/run.n and b/run.n differ diff --git a/src/loreline/Interpreter.hx b/src/loreline/Interpreter.hx index 927aa90..c6064cf 100644 --- a/src/loreline/Interpreter.hx +++ b/src/loreline/Interpreter.hx @@ -1,5 +1,6 @@ package loreline; +import Type.ValueType; import haxe.ds.StringMap; import loreline.Lexer; import loreline.Node; @@ -261,14 +262,22 @@ class Interpreter { final cycleByNode:Map = new Map(); + var _random:Random = null; + function random():Float { + if (_random == null) { + _random = new Random(); + } + return _random.next(); + } + function initializeTopLevelFunctions(functions:Map) { topLevelFunctions.set('random', (min:Int, max:Int) -> { - return Math.floor(min + Math.random() * (max + 1 - min)); + return Math.floor(min + random() * (max + 1 - min)); }); topLevelFunctions.set('chance', (n:Int) -> { - return Math.floor(Math.random() * n) == 0; + return Math.floor(random() * n) == 0; }); topLevelFunctions.set('wait', (seconds:Float) -> { @@ -817,10 +826,10 @@ class Interpreter { final currentValue = switch (assign.op) { case OpAssign: value; - case OpPlusAssign: performOperation(OpPlus, readAccess(target), value); - case OpMinusAssign: performOperation(OpMinus, readAccess(target), value); - case OpMultiplyAssign: performOperation(OpMultiply, readAccess(target), value); - case OpDivideAssign: performOperation(OpDivide, readAccess(target), value); + case OpPlusAssign: performOperation(OpPlus, readAccess(target), value, assign.pos); + case OpMinusAssign: performOperation(OpMinus, readAccess(target), value, assign.pos); + case OpMultiplyAssign: performOperation(OpMultiply, readAccess(target), value, assign.pos); + case OpDivideAssign: performOperation(OpDivide, readAccess(target), value, assign.pos); case _: throw new RuntimeError('Invalid assignment operator', assign.pos); } @@ -1097,7 +1106,7 @@ class Interpreter { final bin:NBinary = cast expr; final left = evaluateExpression(bin.left); final right = evaluateExpression(bin.right); - performOperation(bin.op, left, right); + performOperation(bin.op, left, right, bin.pos); case NUnary: final un:NUnary = cast expr; @@ -1259,29 +1268,107 @@ class Interpreter { } - function performOperation(op:TokenType, left:Dynamic, right:Dynamic):Any { + /** + * Helper for getting human-readable type names in errors + */ + function getTypeName(t:ValueType):String { + return switch t { + case TNull: "Null"; + case TInt: "Int"; + case TFloat: "Float"; + case TBool: "Bool"; + case TObject: "Object"; + case TFunction: "Function"; + case TClass(c): Type.getClassName(c); + case TEnum(e): Type.getEnumName(e); + case TUnknown: "Unknown"; + } + } + + function performOperation(op:TokenType, left:Dynamic, right:Dynamic, pos:Position):Any { + // Get precise runtime types + final leftType = Type.typeof(left); + final rightType = Type.typeof(right); return switch op { - case OpPlus: left + right; - case OpMinus: left - right; - case OpMultiply: left * right; - case OpDivide: - if (right == 0) throw new RuntimeError('Division by zero', currentScope?.node?.pos ?? script.pos); - left / right; - case OpModulo: left % right; - case OpEquals: left == right; - case OpNotEquals: left != right; - case OpGreater: left > right; - case OpGreaterEq: left >= right; - case OpLess: left < right; - case OpLessEq: left <= right; - case OpAnd: left && right; - case OpOr: left || right; + case OpPlus: + switch [leftType, rightType] { + // Number + Number + case [TInt | TFloat, TInt | TFloat]: + Std.parseFloat(Std.string(left)) + Std.parseFloat(Std.string(right)); + // String + Any (allows string concatenation) + case [TClass(String), _] | [_, TClass(String)]: + Std.string(left) + Std.string(right); + case _: + throw new RuntimeError('Cannot add ${getTypeName(leftType)} and ${getTypeName(rightType)}', pos ?? currentScope?.node?.pos ?? script.pos); + } + + case OpMinus | OpMultiply | OpDivide | OpModulo: + switch [leftType, rightType] { + case [TInt | TFloat, TInt | TFloat]: + final leftNum = Std.parseFloat(Std.string(left)); + final rightNum = Std.parseFloat(Std.string(right)); + switch op { + case OpMinus: leftNum - rightNum; + case OpMultiply: leftNum * rightNum; + case OpDivide: + if (rightNum == 0) throw new RuntimeError('Division by zero', pos ?? currentScope?.node?.pos ?? script.pos); + leftNum / rightNum; + case OpModulo: + if (rightNum == 0) throw new RuntimeError('Modulo by zero', pos ?? currentScope?.node?.pos ?? script.pos); + leftNum % rightNum; + case _: throw "Unreachable"; + } + case _: + final opName = switch op { + case OpMinus: "subtract"; + case OpMultiply: "multiply"; + case OpDivide: "divide"; + case OpModulo: "modulo"; + case _: "perform operation on"; + } + throw new RuntimeError('Cannot ${opName} ${getTypeName(leftType)} and ${getTypeName(rightType)}', pos ?? currentScope?.node?.pos ?? script.pos); + } + + case OpEquals | OpNotEquals: + // Allow comparison between any types + switch op { + case OpEquals: left == right; + case OpNotEquals: left != right; + case _: throw "Unreachable"; + } + + case OpGreater | OpGreaterEq | OpLess | OpLessEq: + switch [leftType, rightType] { + case [TInt | TFloat, TInt | TFloat]: + final leftNum = Std.parseFloat(Std.string(left)); + final rightNum = Std.parseFloat(Std.string(right)); + switch op { + case OpGreater: leftNum > rightNum; + case OpGreaterEq: leftNum >= rightNum; + case OpLess: leftNum < rightNum; + case OpLessEq: leftNum <= rightNum; + case _: throw "Unreachable"; + } + case _: + throw new RuntimeError('Cannot compare ${getTypeName(leftType)} and ${getTypeName(rightType)}', pos ?? currentScope?.node?.pos ?? script.pos); + } + + case OpAnd | OpOr: + switch [leftType, rightType] { + case [TBool, TBool]: + switch op { + case OpAnd: left && right; + case OpOr: left || right; + case _: throw "Unreachable"; + } + case _: + throw new RuntimeError('Cannot perform logical operation on ${getTypeName(leftType)} and ${getTypeName(rightType)}', pos ?? currentScope?.node?.pos ?? script.pos); + } case _: - throw new RuntimeError('Invalid operation: $op', currentScope?.node?.pos ?? script.pos); + throw new RuntimeError('Invalid operation: $op', pos ?? currentScope?.node?.pos ?? script.pos); } - } function valueToString(value:Any):String { diff --git a/src/loreline/Lexer.hx b/src/loreline/Lexer.hx index 2486474..9a9833f 100644 --- a/src/loreline/Lexer.hx +++ b/src/loreline/Lexer.hx @@ -34,6 +34,32 @@ enum LStringAttachment { } +enum abstract TokenStackType(Int) { + + var ChoiceBrace; + + var ChoiceIndent; + + var StateBrace; + + var StateIndent; + + var CharacterBrace; + + var CharacterIndent; + + var BeatBrace; + + var BeatIndent; + + var Brace; + + var Indent; + + var Bracket; + +} + /** * Represents the different types of tokens that can be produced by the lexer. */ @@ -134,6 +160,11 @@ enum TokenType { /** Multi-line comment */ CommentMultiLine(content:String); + /** Increase indentation level */ + Indent; + /** Decrease indentation level */ + Unindent; + /** Line break token */ LineBreak; @@ -164,6 +195,8 @@ class TokenTypeHelpers { case [RParen, RParen]: true; case [LBracket, LBracket]: true; case [RBracket, RBracket]: true; + case [Indent, Indent]: true; + case [Unindent, Unindent]: true; case [LineBreak, LineBreak]: true; case [KwState, KwState]: true; case [KwBeat, KwBeat]: true; @@ -227,6 +260,18 @@ class TokenTypeHelpers { } } + /** + * Checks if a token is a block start + * @param a Token type to check + * @return Whether the token type is a block start + */ + public static function isBlockStart(a:TokenType):Bool { + return switch a { + case KwState | KwBeat | KwCharacter | KwChoice | KwIf: true; + case _: false; + } + } + } /** @@ -335,13 +380,13 @@ class Lexer { * A stack to keep track of whether we are inside a `beat` or a `state`/`character` block. * Depending on that, the rules for reading unquoted string tokens are different. */ - var stack:Array; + var stack:Array; /** * The token type that will be added to the `stack` * next time we find a `LBrace` token */ - var nextBlock:TokenType; + var nextBlock:TokenStackType; /** * When higher than zero, that means only strictly @@ -351,6 +396,21 @@ class Lexer { */ var strictExprs:Int; + /** Current indentation level (number of spaces/tabs) */ + var indentLevel:Int = 0; + + /** Stack of indentation levels */ + var indentStack:Array = []; + + /** Queue of generated indentation tokens */ + var indentTokens:Array = []; + + /** The indentation size (e.g., 4 spaces or 1 tab) */ + var indentSize:Int = 4; + + /** Whether tabs are allowed for indentation */ + var allowTabs:Bool = true; + /** * Creates a new lexer for the given input. * @param input The source code to lex @@ -372,9 +432,12 @@ class Lexer { this.startColumn = 1; this.previous = null; this.stack = []; - this.nextBlock = LBrace; + this.nextBlock = Brace; this.tokenized = null; this.strictExprs = 0; + this.indentLevel = 0; + this.indentStack = [0]; + this.indentTokens = []; } /** @@ -386,24 +449,56 @@ class Lexer { this.tokenized = tokens; while (true) { final token = nextToken(); + + // Handle EOF + if (token.type == Eof) { + // Generate any remaining unindents + if (indentStack.length > 1) { + var count = indentStack.length - 1; + for (_ in 0...count) { + tokens.push(makeToken(Unindent)); + } + } + break; + } + tokens.push(token); switch token.type { - case KwState | KwCharacter | KwBeat | KwChoice: - nextBlock = token.type; + case KwState: + nextBlock = StateIndent; + case KwCharacter: + nextBlock = CharacterIndent; + case KwBeat: + nextBlock = BeatIndent; + case KwChoice: + nextBlock = ChoiceIndent; case LBrace: - stack.push(nextBlock); - nextBlock = LBrace; + stack.push(switch nextBlock { + case ChoiceBrace | ChoiceIndent: ChoiceBrace; + case StateBrace | StateIndent: StateBrace; + case CharacterBrace | CharacterIndent: CharacterBrace; + case BeatBrace | BeatIndent: BeatIndent; + case Brace | Indent | Bracket: Brace; + }); + nextBlock = Brace; + case Indent: + stack.push(switch nextBlock { + case ChoiceBrace | ChoiceIndent: ChoiceIndent; + case StateBrace | StateIndent: StateIndent; + case CharacterBrace | CharacterIndent: CharacterIndent; + case BeatBrace | BeatIndent: BeatIndent; + case Brace | Indent | Bracket: Indent; + }); + nextBlock = Brace; case LBracket: - stack.push(LBracket); - nextBlock = LBrace; - case RBrace | RBracket: + stack.push(Bracket); + nextBlock = Brace; + case RBrace | Unindent | RBracket: stack.pop(); - nextBlock = LBrace; + nextBlock = Brace; case _: } - - if (token.type == Eof) break; } return tokens; } @@ -414,6 +509,11 @@ class Lexer { * @throws LexerError if invalid input is encountered */ public function nextToken():Token { + // Check for queued indentation tokens first + if (indentTokens.length > 0) { + return indentTokens.shift(); + } + skipWhitespace(); if (pos >= length) { @@ -425,7 +525,24 @@ class Lexer { final c = input.charCodeAt(pos); if (c == "\n".code || c == "\r".code) { - return readLineBreak(); + final lineBreakToken = readLineBreak(); + + // After a line break, check for indentation changes + var currentIndent = countIndentation(); + + if (currentIndent > indentStack[indentStack.length - 1]) { + // Indent - just check that it's more than previous level + indentStack.push(currentIndent); + indentTokens.push(makeToken(Indent)); + } else if (currentIndent < indentStack[indentStack.length - 1]) { + // Unindent - pop until we find a matching or lower level + while (indentStack.length > 0 && currentIndent < indentStack[indentStack.length - 1]) { + indentStack.pop(); + indentTokens.push(makeToken(Unindent)); + } + } + + return lineBreakToken; } final startPos = makePosition(); @@ -565,43 +682,56 @@ class Lexer { } - /** - * Returns the token type of the parent block. - * @return The token type of the parent block or KwBeat if at top level - */ - function parentBlockType():TokenType { + function countIndentation():Int { + var pos = this.pos; + var spaces = 0; - var i = stack.length - 1; - while (i >= 0) { - if (stack[i] != LBrace && stack[i] != LBracket) { - if (stack[i] == KwChoice && i < stack.length - 1) { - return KwBeat; - } - return stack[i]; + // Count spaces/tabs + while (pos < length) { + final c = input.charCodeAt(pos); + if (c == " ".code) { + spaces++; + } else if (c == "\t".code) { + spaces += 4; // Treat each tab as 4 spaces + } else { + break; } - i--; + pos++; } - // Assume top level is like being in a beat - return KwBeat; + // Check if line is empty or only whitespace + if (pos >= length || input.charCodeAt(pos) == "\n".code || input.charCodeAt(pos) == "\r".code) { + // Return previous indentation level for empty lines + return indentStack[indentStack.length - 1]; + } + return spaces; } /** - * Checks if currently in a parent bracket block. - * @return True if inside brackets, false otherwise + * Returns the token type of the parent block. + * @return The token type of the parent block or KwBeat if at top level */ - function inParentBrackets():Bool { + function parentBlockType():TokenType { var i = stack.length - 1; while (i >= 0) { - if (stack[i] != LBrace) { - return stack[i] == LBracket; + if (stack[i] != Brace && stack[i] != Indent && stack[i] != Bracket) { + return switch stack[i] { + case ChoiceBrace | ChoiceIndent: KwBeat; + case StateBrace | StateIndent: KwState; + case CharacterBrace | CharacterIndent: KwCharacter; + case BeatBrace | BeatIndent: KwBeat; + case Brace: LBrace; + case Indent: Indent; + case Bracket: LBracket; + }; } i--; } - return false; + // Assume top level is like being in a beat + return KwBeat; } @@ -682,56 +812,236 @@ class Lexer { return input.substr(pos, identifierLength); } + /** + * Helper function to skip whitespace and comments + */ + function skipWhitespaceAndComments(pos:Int):Int { + final startPos = pos; + var foundContent = false; + while (pos < input.length) { + // Skip whitespace + while (pos < input.length && (input.charCodeAt(pos) == " ".code || input.charCodeAt(pos) == "\t".code)) { + pos++; + foundContent = true; + } + + // Check for comments + if (pos < input.length - 1) { + if (input.charCodeAt(pos) == "/".code) { + if (input.charCodeAt(pos + 1) == "/".code) { + // Single line comment - invalid in single line + pos = startPos; + return pos; + } + else if (input.charCodeAt(pos + 1) == "*".code) { + // Multi-line comment + pos += 2; + foundContent = true; + var commentClosed = false; + while (pos < input.length - 1) { + if (input.charCodeAt(pos) == "*".code && input.charCodeAt(pos + 1) == "/".code) { + pos += 2; + commentClosed = true; + break; + } + pos++; + } + if (!commentClosed) { + pos = startPos; + return pos; + } + continue; + } + } + } + break; + } + return foundContent ? pos : startPos; + } + /** * Returns whether the input at the given position is the start of an if condition. * @param pos Position to check from * @return True if an if condition starts at the position, false otherwise */ function isIfStart(pos:Int):Bool { - + // Check "if" literal first if (input.charCodeAt(pos) != "i".code) return false; pos++; if (input.charCodeAt(pos) != "f".code) return false; pos++; - final len = input.length; - var inComment = false; - var matchesIf = false; + // Save initial position to restore it later + var startPos = pos; - while (pos < len) { - var c = input.charCodeAt(pos); - var cc = pos < input.length - 1 ? input.charCodeAt(pos+1) : 0; + // Helper function to read identifier + inline function readIdent():Bool { + var result = true; - if (inComment) { - if (c == "*".code && cc == "/".code) { - inComment = false; - pos += 2; + if (pos >= input.length) { + result = false; + } + else { + var c = input.charCodeAt(pos); + + // First char must be letter or underscore + if (!isIdentifierStart(c)) { + result = false; } else { pos++; + + // Continue reading identifier chars + while (pos < input.length) { + c = input.charCodeAt(pos); + if (!isIdentifierPart(c)) break; + pos++; + } } } - else { - if (c == "/".code && cc == "*".code) { - inComment = true; - pos += 2; + + return result; + } + + pos = skipWhitespaceAndComments(pos); + + // Handle optional ! for negation + if (pos < input.length && input.charCodeAt(pos) == "!".code) { + pos++; + pos = skipWhitespaceAndComments(pos); + } + + // If directly followed with (, that's a valid if + if (input.charCodeAt(pos) == "(".code) { + return true; + } + + // If "if" is directly followed by an identifier start, that's not a if + if (pos == startPos && isIdentifierStart(input.charCodeAt(startPos))) { + return false; + } + + // Must start with identifier or opening parenthesis + if (pos >= input.length || !isIdentifierStart(input.charCodeAt(pos))) { + return false; + } + + while (pos < input.length) { + if (input.charCodeAt(pos) == "(".code) { + // Function call + return true; + } else { + if (!readIdent()) { + return false; } - else if (isWhitespace(c)) { - pos++; + } + + pos = skipWhitespaceAndComments(pos); + if (pos >= input.length) { + return true; + } + + var c = input.charCodeAt(pos); + + // Handle dot access + if (c == ".".code) { + pos++; + pos = skipWhitespaceAndComments(pos); + if (!readIdent()) { + return false; } - else if (c == "(".code) { - matchesIf = true; - break; + pos = skipWhitespaceAndComments(pos); + if (pos >= input.length) { + return true; } - else { - break; + c = input.charCodeAt(pos); + } + + // Handle bracket access + if (c == "[".code) { + pos++; + var bracketLevel = 1; + while (pos < input.length && bracketLevel > 0) { + c = input.charCodeAt(pos); + if (c == "[".code) bracketLevel++; + if (c == "]".code) bracketLevel--; + pos++; + } + pos = skipWhitespaceAndComments(pos); + if (pos >= input.length) { + return true; } + c = input.charCodeAt(pos); + } + + // Check for various delimiters typical from if condition + if (c == "(".code || c == "&".code || c == "|".code || ((input.charCodeAt(pos + 1) == "=".code) && c == "=".code) || c == ">".code || c == "<".code || (input.charCodeAt(pos + 1) != "=".code && (c == "+".code || c == "-".code || c == "*".code || c == "/".code))) { + return true; } + + // If we're at end or newline, it's valid + if (c == "\n".code || c == "\r".code || pos >= input.length) { + pos = startPos; + return true; + } + + // Any other character invalidates it + return false; } - return matchesIf; + // If we get here, we're at end of input + return true; + } + + /** + * Returns whether the input at the given position is a valid transition start. + * A valid transition consists of "->" followed by an identifier, with optional + * whitespace and comments in between. Nothing but whitespace and comments can + * follow the identifier. + * @param pos Position to check from + * @return True if a valid transition starts at the position, false otherwise + */ + function isTransitionStart(pos:Int):Bool { + // Save initial position to restore it later + var startPos = pos; + + // Check for -> + if (input.charCodeAt(pos) != "-".code || pos >= input.length - 1 || input.charCodeAt(pos + 1) != ">".code) { + return false; + } + pos += 2; + + // Skip whitespace and comments between -> and identifier + pos = skipWhitespaceAndComments(pos); + + // Read identifier + if (pos >= input.length || !isIdentifierStart(input.charCodeAt(pos))) { + pos = startPos; + return false; + } + + // Move past identifier + pos++; + while (pos < input.length && isIdentifierPart(input.charCodeAt(pos))) { + pos++; + } + + // Skip any trailing comments + pos = skipWhitespaceAndComments(pos); + + // Check that we're at end of line, end of input, or only have whitespace/comments left + if (pos < input.length) { + var c = input.charCodeAt(pos); + if (c != "\n".code && c != "\r".code && c != " ".code && c != "\t".code && c != "/".code) { + pos = startPos; + return false; + } + } + // Restore original position and return success + pos = startPos; + return true; } /** @@ -744,13 +1054,103 @@ class Lexer { // Save initial position to restore it later var startPos = pos; - // Helper function to skip whitespace - inline function skipWhitespaces() { - while (pos < input.length && (input.charCodeAt(pos) == " ".code || input.charCodeAt(pos) == "\t".code)) { + // Helper function to read identifier + inline function readIdent():Bool { + var result = true; + + if (pos >= input.length) { + result = false; + } + else { + var c = input.charCodeAt(pos); + + // First char must be letter or underscore + if (!isIdentifierStart(c)) { + result = false; + } + else { + pos++; + + // Continue reading identifier chars + while (pos < input.length) { + c = input.charCodeAt(pos); + if (!isIdentifierPart(c)) break; + pos++; + } + } + } + + return result; + } + + // Must start with identifier + if (!readIdent()) { + pos = startPos; + return false; + } + + // Keep reading segments until we find opening parenthesis + while (pos < input.length) { + pos = skipWhitespaceAndComments(pos); + + if (pos >= input.length) { + pos = startPos; + return false; + } + + var c = input.charCodeAt(pos); + + // Found opening parenthesis - success! + if (c == "(".code) { + pos = startPos; + return true; + } + + // Handle dot access + if (c == ".".code) { pos++; + pos = skipWhitespaceAndComments(pos); + if (!readIdent()) { + pos = startPos; + return false; + } + continue; } + + // Handle bracket access + if (c == "[".code) { + // Skip everything until closing bracket + pos++; + while (pos < input.length) { + if (input.charCodeAt(pos) == "]".code) { + pos++; + break; + } + pos++; + } + continue; + } + + // Any other character means this isn't a call + pos = startPos; + return false; } + pos = startPos; + return false; + + } + + /** + * Returns whether the input at the given position is the start of an assignment. + * @param pos Position to check from + * @return True if an assignment starts at the position, false otherwise + */ + function isAssignStart(pos:Int):Bool { + + // Save initial position to restore it later + var startPos = pos; + // Helper function to read identifier inline function readIdent():Bool { var result = true; @@ -788,7 +1188,7 @@ class Lexer { // Keep reading segments until we find opening parenthesis while (pos < input.length) { - skipWhitespaces(); + pos = skipWhitespaceAndComments(pos); if (pos >= input.length) { pos = startPos; @@ -797,8 +1197,9 @@ class Lexer { var c = input.charCodeAt(pos); - // Found opening parenthesis - success! - if (c == "(".code) { + // Found assign operator + if (c == "=".code || + (input.charCodeAt(pos + 1) == "=".code && (c == "+".code || c == "-".code || c == "*".code || c == "/".code))) { pos = startPos; return true; } @@ -806,7 +1207,7 @@ class Lexer { // Handle dot access if (c == ".".code) { pos++; - skipWhitespaces(); + pos = skipWhitespaceAndComments(pos); if (!readIdent()) { pos = startPos; return false; @@ -828,7 +1229,7 @@ class Lexer { continue; } - // Any other character means this isn't a call + // Any other character means this isn't an assign pos = startPos; return false; } @@ -866,7 +1267,7 @@ class Lexer { while (i >= 0) { final token = tokenized[i]; - if (!token.type.isComment() && token.type != LineBreak) { + if (!token.type.isComment() && token.type != LineBreak && token.type != Indent && token.type != Unindent) { return (token.type == Colon && i > 0 && tokenized[i-1].type.isIdentifier()); } @@ -883,7 +1284,11 @@ class Lexer { */ function isInsideBrackets():Bool { - return stack.length > 0 && stack[stack.length-1] == LBracket; + var i = stack.length - 1; + while (i >= 0 && stack[stack.length-1] == Indent) { + i--; + } + return i >= 0 && stack[i] == Bracket; } @@ -898,7 +1303,7 @@ class Lexer { var i = tokenized.length - 1; while (i >= 0) { final token = tokenized[i]; - if (token.type.isComment()) { + if (token.type.isComment() || token.type == Indent || token.type == Unindent) { i--; } else if (!foundLabel && token.type == Colon) { @@ -935,7 +1340,7 @@ class Lexer { while (i >= 0) { final token = tokenized[i]; - if (token.type.isComment()) { + if (token.type.isComment() || token.type == Indent || token.type == Unindent) { i--; } else if (token.type == LineBreak) { @@ -1003,6 +1408,7 @@ class Lexer { * @return Token if an unquoted string was read, null otherwise */ function tryReadUnquotedString():Null { + // Skip in strict expression area if (strictExprs > 0) return null; @@ -1032,14 +1438,14 @@ class Lexer { // Check what is the parent block final parent = parentBlockType(); - // Skip if parent is not a beat, character, state or choice - if (parent != KwBeat && parent != KwState && parent != KwCharacter && parent != KwChoice) { + // Skip if parent is not a beat, character, state + if (parent != KwBeat && parent != KwState && parent != KwCharacter) { return null; } // Skip if this is a condition start or call start, in a beat block if (parent == KwBeat) { - if (isIfStart(pos) || isCallStart(pos)) { + if (isIfStart(pos) || isCallStart(pos) || isAssignStart(pos)) { return null; } } @@ -1050,11 +1456,9 @@ class Lexer { // -= // *= // /= - // -> if (parent == KwBeat) { if (c == "=".code || - (cc == "=".code && (c == "+".code || c == "-".code || c == "*".code || c == "/".code)) || - (cc == ">".code && (c == "-".code))) { + (cc == "=".code && (c == "+".code || c == "-".code || c == "*".code || c == "/".code))) { return null; } } @@ -1073,6 +1477,9 @@ class Lexer { // By default, tags are not allowed unless inside beat content var allowTags = (parent == KwBeat); + // Tells whether we are reading a dialogue text or not + var isDialogue = false; + // If inside a character or state block, // Only allow unquoted strings after labels (someKey: ...) // or inside array brackets @@ -1084,26 +1491,23 @@ class Lexer { } } - // If inside a choice, - // Only allow unquoted strings preceded by - // white spaces or comments in current line - else if (parent == KwChoice) { - - // Skip if not starting line - if (!followsOnlyWhitespacesOrCommentsInLine()) { - return null; - } - } - // If inside a beat, // Only allow unquoted strings after labels (someKey: ...) starting the line // or if only preceded by white spaces or comments in current line else if (parent == KwBeat) { // Skip if not after a label or starting line - if (!followsOnlyLabelOrCommentsInLine() && !followsOnlyWhitespacesOrCommentsInLine()) { + isDialogue = followsOnlyLabelOrCommentsInLine(); + if (!isDialogue && !followsOnlyWhitespacesOrCommentsInLine()) { return null; } + + if (!isDialogue) { + // When not in dialogue, in beat, forbid starting with arrow + if (cc == ">".code && (c == "-".code)) { + return null; + } + } } // If we get here, we can start reading the unquoted string @@ -1153,17 +1557,15 @@ class Lexer { break; } // Check for trailing if - else if (tagStart == -1 && parent == KwChoice && c == "i".code && input.charCodeAt(pos+1) == "f".code && isIfStart(pos)) { + else if (tagStart == -1 && parent == KwBeat && c == "i".code && input.charCodeAt(pos+1) == "f".code && isIfStart(pos)) { break; } // Check for comment start else if (tagStart == -1 && (c == "/".code && pos < length - 1 && (input.charCodeAt(pos+1) == "/".code || input.charCodeAt(pos+1) == "*".code))) { break; } - // No assign in beats - else if (tagStart == -1 && parent == KwBeat && (c == "=".code || - (input.charCodeAt(pos+1) == "=".code && (c == "+".code || c == "-".code || c == "*".code || c == "/".code)))) { - valid = false; + // Check for arrow start + else if (tagStart == -1 && c == "-".code && pos < length - 1 && input.charCodeAt(pos+1) == ">".code && isTransitionStart(pos)) { break; } else if (allowTags && c == "<".code) { @@ -1444,6 +1846,9 @@ class Lexer { currentLine++; currentColumn = 1; } + else if (token.type == Indent || token.type == Unindent) { + // Ignore + } else { var tokenLength = switch (token.type) { case Identifier(name): name.length; diff --git a/src/loreline/Parser.hx b/src/loreline/Parser.hx index 6aaee7e..0797ec3 100644 --- a/src/loreline/Parser.hx +++ b/src/loreline/Parser.hx @@ -343,14 +343,15 @@ class Parser { * @return Array of parsed statement nodes */ function parseStatementBlock():Array { - expect(LBrace); + + final blockEnd:TokenType = parseBlockStart().type == Indent ? Unindent : RBrace; final statements:Array = []; - while (!check(RBrace) && !isAtEnd()) { + while (!check(blockEnd) && !isAtEnd()) { // Handle line breaks and comments - while (match(LineBreak)) {} + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} - if (check(RBrace)) break; + if (check(blockEnd)) break; // Parse statement try { @@ -359,13 +360,13 @@ class Parser { if (errors == null) errors = []; errors.push(e); synchronize(); - if (check(RBrace)) break; + if (check(blockEnd)) break; } - while (match(LineBreak)) {} + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} } - expect(RBrace); + expect(blockEnd); return statements; } @@ -379,7 +380,7 @@ class Parser { final stateNode = new NStateDecl(nextNodeId++, startPos, temporary, null); expect(KwState); - while (match(LineBreak)) {} // Optional breaks before { + while (match(LineBreak)) {} attachComments(stateNode); @@ -417,48 +418,97 @@ class Parser { expect(KwBeat); beatNode.name = expectIdentifier(); - while (match(LineBreak)) {} // Optional before block - expect(LBrace); - while (match(LineBreak)) {} // Optional after { - - var braceLevel = 1; + final blockEnd:TokenType = parseBlockStart().type == Indent ? Unindent : RBrace; attachComments(beatNode); - // Parse beat body with proper brace level tracking - while (!isAtEnd() && braceLevel > 0) { - switch (tokens[current].type) { - case RBrace: - braceLevel--; - if (braceLevel > 0) advance(); - case LBrace: - braceLevel++; - advance(); - case _: - if (braceLevel == 1) { - beatNode.body.push(parseNode()); - } - else { - advance(); - } - } + // Parse character properties + while (!check(blockEnd) && !isAtEnd()) { + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} + beatNode.body.push(parseNode()); + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} } - expect(RBrace); + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} + expect(blockEnd); + return beatNode; } - /** - * Checks if the current token begins a block construct. - * @return True if current token starts a block - */ - function isBlockStart():Bool { - return switch (tokens[current].type) { - case KwIf | KwChoice | Arrow: true; - case Identifier(_): - peek().type == Arrow; - case _: false; + function checkBlockStart():Bool { + + var indentToken:Token = null; + var braceToken:Token = null; + var numIndents = 0; + var i = 0; + while (current + i < tokens.length) { + final token = tokens[current + i]; + i++; + + if (token.type == LineBreak) continue; + if (token.type == Indent) { + numIndents++; + indentToken = token; + continue; + } + if (token.type == LBrace) { + if (braceToken == null) { + braceToken = token; + } + continue; + } + break; + } + + if (braceToken != null) { + return true; + } + else if (indentToken != null) { + if (numIndents > 1) { + throw new ParseError('Invalid indentation level', indentToken.pos); + } + return true; + } + else { + return false; + } + + } + + function parseBlockStart():Token { + + var indentToken:Token = null; + var braceToken:Token = null; + var numIndents = 0; + while (true) { + if (match(LineBreak)) continue; + if (match(Indent)) { + numIndents++; + indentToken = previous(); + continue; + } + if (match(LBrace)) { + if (braceToken == null) { + braceToken = previous(); + } + continue; + } + break; + } + + if (braceToken != null) { + return braceToken; + } + else if (indentToken != null) { + if (numIndents > 1) { + throw new ParseError('Invalid indentation level', indentToken.pos); + } + return indentToken; } + else { + throw new ParseError('Expected ${TokenType.LBrace} or ${TokenType.Indent}, got ${tokens[current].type}', tokens[current].pos); + } + } /** @@ -472,20 +522,19 @@ class Parser { expect(KwCharacter); characterNode.name = expectIdentifier(); - while (match(LineBreak)) {} // Optional before block - expect(LBrace); - while (match(LineBreak)) {} // Optional after { + final blockEnd:TokenType = parseBlockStart().type == Indent ? Unindent : RBrace; attachComments(characterNode); // Parse character properties - while (!check(RBrace) && !isAtEnd()) { + while (!check(blockEnd) && !isAtEnd()) { + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} characterNode.properties.push(parseObjectField()); - while (match(LineBreak)) {} // Optional between fields + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} } - while (match(LineBreak)) {} // Optional before } - expect(RBrace); + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} + expect(blockEnd); return characterNode; } @@ -510,20 +559,19 @@ class Parser { final choiceNode = new NChoiceStatement(nextNodeId++, startPos, []); expect(KwChoice); - while (match(LineBreak)) {} - expect(LBrace); - while (match(LineBreak)) {} + final blockEnd:TokenType = parseBlockStart().type == Indent ? Unindent : RBrace; attachComments(choiceNode); // Parse choice options - while (!check(RBrace) && !isAtEnd()) { - choiceNode.options.push(parseChoiceOption()); - while (match(LineBreak)) {} + while (!check(blockEnd) && !isAtEnd()) { + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} + choiceNode.options.push(parseChoiceOption(blockEnd)); + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} } - expect(RBrace); + expect(blockEnd); return choiceNode; } @@ -532,28 +580,22 @@ class Parser { * Parses a single choice option with its condition and consequences. * @return Choice option node */ - function parseChoiceOption():NChoiceOption { + function parseChoiceOption(blockEnd:TokenType):NChoiceOption { final startPos = tokens[current].pos; final choiceOption = attachComments(new NChoiceOption(nextNodeId++, startPos, null, null, [])); choiceOption.text = parseStringLiteral(); // Parse optional condition if (match(KwIf)) { - choiceOption.condition = parseParenExpression(); + choiceOption.condition = parseConditionExpression(); } // Parse option body - if (check(LBrace)) { + if (checkBlockStart()) { choiceOption.body = parseStatementBlock(); } - else if (!check(RBrace)) { // If not end of choice - // Handle single statement bodies - if (checkString()) { - choiceOption.body = [parseTextStatement()]; - } - else { - choiceOption.body = [parseNode()]; - } + else if (!check(blockEnd)) { // If not end of choice + choiceOption.body = [parseNode()]; } return choiceOption; @@ -612,7 +654,7 @@ class Parser { final ifNode = new NIfStatement(nextNodeId++, startPos, null, null, null); expect(KwIf); - ifNode.condition = parseParenExpression(); + ifNode.condition = parseConditionExpression(); while (match(LineBreak)) {} attachComments(ifNode); @@ -621,7 +663,6 @@ class Parser { ifNode.thenBranch.body = parseStatementBlock(); // Handle optional else clause - var elseBranch:Null> = null; var elseToken = tokens[current]; if (elseToken.type == KwElse) { advance(); @@ -934,7 +975,7 @@ class Parser { return stringLiteral; case _: - throw new ParseError("Expected string", tokens[current].pos); + throw new ParseError('Expected string, got ${tokens[current].type}', tokens[current].pos); } } @@ -1242,34 +1283,33 @@ class Parser { final fields = []; final literal = new NLiteral(nextNodeId++, startPos, fields, Object); - expect(LBrace); - while (match(LineBreak)) {} // Optional breaks after { + final blockEnd:TokenType = parseBlockStart().type == Indent ? Unindent : RBrace; attachComments(literal); var needsSeparator = false; - while (!check(RBrace) && !isAtEnd()) { + while (!check(blockEnd) && !isAtEnd()) { // Handle separators between fields if (needsSeparator) { - while (match(LineBreak)) { + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) { needsSeparator = false; } if (match(Comma)) { needsSeparator = false; } } - while (match(LineBreak)) { + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) { needsSeparator = false; } - if (!check(RBrace) && needsSeparator) { + if (!check(blockEnd) && needsSeparator) { throw new ParseError("Expected comma or line break between fields", tokens[current].pos); } - while (match(LineBreak)) {} + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} - if (!check(RBrace)) { + if (!check(blockEnd)) { fields.push(parseObjectField()); } @@ -1277,8 +1317,8 @@ class Parser { needsSeparator = (prev.type != Colon && prev.type != LineBreak); } - while (match(LineBreak)) {} - expect(RBrace); + while (match(LineBreak) || (blockEnd != Unindent && match(Unindent))) {} + expect(blockEnd); return literal; } @@ -1301,13 +1341,13 @@ class Parser { } /** - * Parses a parenthesized expression. + * Parses a condition expression. * @return Expression node */ - function parseParenExpression():NExpr { - expect(LParen); + function parseConditionExpression():NExpr { + final hasParen = match(LParen); final expr = parseExpression(); - expect(RParen); + if (hasParen) expect(RParen); return expr; } @@ -1342,7 +1382,7 @@ class Parser { if (check(type)) { return advance(); } - throw new ParseError('Expected ${type}, got ${tokens[current].type}', tokens[current].pos); + throw new ParseError('Expected ${type}, got ${isAtEnd() ? 'EoF' : Std.string(tokens[current].type)}', tokens[Std.int(Math.min(current, tokens.length - 1))].pos); } /** @@ -1356,7 +1396,7 @@ class Parser { advance(); name; case _: - throw new ParseError('Expected identifier', tokens[current].pos); + throw new ParseError('Expected identifier, got ${tokens[current].type}', tokens[current].pos); } } diff --git a/src/loreline/Random.hx b/src/loreline/Random.hx new file mode 100644 index 0000000..7d332c1 --- /dev/null +++ b/src/loreline/Random.hx @@ -0,0 +1,81 @@ +package loreline; + +// Based on code from Luxe Engine https://github.com/underscorediscovery/luxe/blob/66bed0cf1a38e58355c65497f8b97de5732467c5/luxe/utils/Random.hx +// itself based on code from http://blog.gskinner.com/archives/2008/01/source_code_see.html +// with license: +// Rndm by Grant Skinner. Jan 15, 2008 +// Visit www.gskinner.com/blog for documentation, updates and more free code. +// Incorporates implementation of the Park Miller (1988) "minimal standard" linear +// congruential pseudo-random number generator by Michael Baczynski, www.polygonal.de. +// (seed * 16807) % 2147483647 +// Copyright (c) 2008 Grant Skinner +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +/** + * Seeded random number generator to get reproducible sequences of values. + */ +class Random { + + public var seed(default, null):Float; + + public var initialSeed(default, null):Float; + + inline public function new(seed:Float = -1) { + + if (seed < 0) { + #if sys + seed = Sys.time() * 960; + #else + seed = new Date().getTime() * 960; + #end + } + + this.seed = seed; + initialSeed = this.seed; + + } + + // Public API + + /** + * Returns a float number between [0,1) + */ + public inline function next():Float { + return (seed = (seed * 16807) % 0x7FFFFFFF) / 0x7FFFFFFF + 0.000000000233; + } + + /** + * Return an integer between [min, max). + */ + public inline function between(min:Int, max:Int):Int { + return Math.floor(min + (max - min) * next()); + } + + /** + * Reset the initial value to that of the current seed. + */ + public inline function reset(?initialSeed:Float) { + if (initialSeed != null) { + this.initialSeed = initialSeed; + } + seed = this.initialSeed; + } + +}