From 66a80f2d97c17399473ebdf3ec70f5e0b049d5cc Mon Sep 17 00:00:00 2001 From: Jeremy Faivre Date: Sun, 12 Jan 2025 17:11:04 +0100 Subject: [PATCH] Make braces optional in all places, parenthesis optional around conditions, given that indentation is respected --- cli/loreline/Cli.hx | 4 + run.n | Bin 171411 -> 182015 bytes src/loreline/Interpreter.hx | 137 +++++++-- src/loreline/Lexer.hx | 591 ++++++++++++++++++++++++++++++------ src/loreline/Parser.hx | 210 +++++++------ src/loreline/Random.hx | 81 +++++ 6 files changed, 820 insertions(+), 203 deletions(-) create mode 100644 src/loreline/Random.hx 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 59cec2161157e89ad02d1e6d762c4198735fe368..a7c3411b8f865bf13aef9aa7d657ec4df70ba8fc 100644 GIT binary patch delta 37857 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 V{=e)mnUr!{dPyZrS$HqF{sB!3o;d&j delta 30003 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 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; + } + +}