Skip to content

Commit

Permalink
First iteration on language server protocol, and many position fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyfa committed Jan 25, 2025
1 parent bf2d97d commit 147aeb6
Show file tree
Hide file tree
Showing 16 changed files with 2,544 additions and 264 deletions.
3 changes: 1 addition & 2 deletions build.hxml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
--class-path src
--class-path cli
--main loreline.Cli
--main loreline.cli.Cli
47 changes: 30 additions & 17 deletions src/loreline/Interpreter.hx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import haxe.ds.StringMap;
import loreline.Lexer;
import loreline.Node;

using StringTools;
using loreline.Utf8;


/**
* A state during the runtime execution of a loreline script
*/
Expand Down Expand Up @@ -56,7 +60,7 @@ enum RuntimeAccess {
* When exiting that scope, the related temporary states associated to it are destroyed
*/
@:structInit
class Scope {
class RuntimeScope {

/**
* The scope id, a unique integer value in the stack
Expand Down Expand Up @@ -198,13 +202,13 @@ class Interpreter {
* The current execution stack, which consists of scopes added on top of one another.
* Each scope can have its own local beats and temporary states.
*/
final stack:Array<Scope> = [];
final stack:Array<RuntimeScope> = [];

/**
* Current scope associated with current execution state
*/
var currentScope(get,never):Scope;
function get_currentScope():Scope {
var currentScope(get,never):RuntimeScope;
function get_currentScope():RuntimeScope {
return stack.length > 0 ? stack[stack.length - 1] : null;
}

Expand Down Expand Up @@ -479,7 +483,7 @@ class Interpreter {

}

function push(scope:Scope):Void {
function push(scope:RuntimeScope):Void {

scope.id = nextScopeId++;
stack.push(scope);
Expand Down Expand Up @@ -531,7 +535,7 @@ class Interpreter {
}


function initializeState(state:NStateDecl, scope:Scope) {
function initializeState(state:NStateDecl, scope:RuntimeScope) {

var runtimeState:RuntimeState = null;
if (state.temporary) {
Expand Down Expand Up @@ -918,18 +922,27 @@ class Interpreter {
}

function evaluateString(str:NStringLiteral):{text:String, tags:Array<TextTag>} {
final buf = new StringBuf();
final buf = new loreline.Utf8.Utf8Buf();
final tags:Array<TextTag> = [];
var offset = 0;

for (part in str.parts) {
var keepWhitespace = (str.quotes != Unquoted);

for (i in 0...str.parts.length) {
final part = str.parts[i];

switch (part.type) {
case Raw(text):
offset += text.length;
if (!keepWhitespace) {
text = text.ltrim();
}
final len = text.uLength();
if (len > 0) keepWhitespace = false;
offset += len;
buf.add(text);

case Expr(expr):

keepWhitespace = false;
if (expr is NAccess) {
// When providing a character object,
// implicitly read the character's `name` field
Expand All @@ -940,20 +953,20 @@ class Interpreter {
final characterFields = evaluateExpression(expr);
final value = getField(characterFields, 'name') ?? name;
final text = valueToString(value);
offset += text.length;
offset += text.uLength();
buf.add(text);

case _:
final value = evaluateExpression(expr);
final text = valueToString(value);
offset += text.length;
offset += text.uLength();
buf.add(text);
}
}
else {
final value = evaluateExpression(expr);
final text = valueToString(value);
offset += text.length;
offset += text.uLength();
buf.add(text);
}

Expand Down Expand Up @@ -1066,7 +1079,7 @@ class Interpreter {
case Number, Boolean, Null: lit.value;
case Array:
[for (elem in (lit.value:Array<Dynamic>)) evaluateExpression(elem)];
case Object:
case Object(_):
final obj = new Map<String, Any>();
for (field in (lit.value:Array<NObjectField>)) {
obj.set(field.name, evaluateExpression(field.value));
Expand Down Expand Up @@ -1354,12 +1367,12 @@ class Interpreter {
throw new RuntimeError('Cannot compare ${getTypeName(leftType)} and ${getTypeName(rightType)}', pos ?? currentScope?.node?.pos ?? script.pos);
}

case OpAnd | OpOr:
case OpAnd(_) | OpOr(_):
switch [leftType, rightType] {
case [TBool, TBool]:
switch op {
case OpAnd: left && right;
case OpOr: left || right;
case OpAnd(_): left && right;
case OpOr(_): left || right;
case _: throw "Unreachable";
}
case _:
Expand Down
194 changes: 194 additions & 0 deletions src/loreline/Lens.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package loreline;

import loreline.Node;
import loreline.Position;

/**
* Definition reference found in a script
*/
@:structInit class Definition {
/** The node where this definition appears */
public var node:Node;
/** The exact position of the definition */
public var pos:Position;
/** Any references to this definition */
public var references:Array<Reference> = [];
}

/**
* Reference to a definition found in a script
*/
@:structInit class Reference {
/** The node containing this reference */
public var node:Node;
/** The exact position of the reference */
public var pos:Position;
}

/**
* Utility class for analyzing Loreline scripts without executing them.
* Provides methods for finding nodes, variables, references, etc.
*/
class Lens {
/** The script being analyzed */
final script:Script;

/** Map of all nodes by their unique ID */
final nodesById:Map<Int, Node> = [];

/** Map of node IDs to their parent nodes */
final parentNodes:Map<Int, Node> = [];

/** Map of node IDs to their child nodes */
final childNodes:Map<Int, Array<Node>> = [];

public function new(script:Script) {
this.script = script;
initialize();
}

/**
* Initialize all the lookups and analysis data
*/
function initialize() {
// First pass: Build node maps and collect definitions
script.each((node, parent) -> {
// Track nodes by ID
nodesById.set(node.id, node);

// Track parent relationships
if (parent != null) {
parentNodes.set(node.id, parent);

// And track the other way around
var children = childNodes.get(parent.id);
if (children == null) {
children = [];
childNodes.set(parent.id, children);
}
children.push(node);
}
});
}

/**
* Gets the nodes at the given position
* @param pos Position to check
* @return Most specific node at that position, or null if none found
*/
public function getNodeAtPosition(pos:Position):Null<Node> {
var bestMatch:Null<Node> = null;

script.each((node, parent) -> {
final nodePos = node.pos;
if (nodePos.length > 0 &&
nodePos.offset <= pos.offset &&
nodePos.offset + nodePos.length >= pos.offset) {

bestMatch = node;
}
});

return bestMatch;
}

/**
* Gets all nodes of a specific type
* @param nodeType Class type to find
* @return Array of matching nodes
*/
public function getNodesOfType<T:Node>(nodeType:Class<T>):Array<T> {
final matches:Array<T> = [];
script.each((node, _) -> {
if (Std.isOfType(node, nodeType)) {
matches.push(cast node);
}
});
return matches;
}

/**
* Gets the parent node of a given node
* @param node Child node
* @return Parent node or null if none found
*/
public function getParentNode(node:Node):Null<Node> {
return parentNodes.get(node.id);
}

/**
* Gets the parent node of a given node
* @param node Child node
* @return Parent node or null if none found
*/
public function getParentOfType<T:Node>(node:Node, type:Class<T>):Null<T> {
var current:Any = node;
while (current != null) {
current = getParentNode(current);
if (current != null && Type.getClass(current) == type) {
return current;
}
}
return null;
}

/**
* Gets all ancestor nodes of a given node
* @param node Starting node
* @return Array of ancestor nodes from immediate parent to root
*/
public function getAncestors(node:Node):Array<Node> {
final ancestors:Array<Node> = [];
var current = node;
while (current != null) {
current = parentNodes.get(current.id);
if (current != null) {
ancestors.push(current);
}
}
return ancestors;
}

/**
* Finds all nodes that match a predicate function
* @param predicate Function that returns true for matching nodes
* @return Array of matching nodes
*/
public function findNodes(predicate:(node:Node) -> Bool):Array<Node> {
final matches:Array<Node> = [];
script.each((node, _) -> {
if (predicate(node)) {
matches.push(node);
}
});
return matches;
}

/**
* Finds and returns the beat declaration referenced by the given transition.
* This method searches through the beat declarations to find a match based on the transition's properties.
* @param transition The transition object containing the reference to search for
* @return The referenced beat declaration if found, null otherwise
*/
public function findReferencedBeat(transition:NTransition):Null<NBeatDecl> {

var result:Null<NBeatDecl> = null;

var parent = parentNodes.get(transition.id);
while (result == null && parent != null) {
parent.each((node, _) -> {
if (Type.getClass(node) == NBeatDecl) {
final beatDecl:NBeatDecl = cast node;
if (beatDecl.name == transition.target) {
result = beatDecl;
}
}
});
parent = parentNodes.get(parent.id);
}

return result;

}

}
Loading

0 comments on commit 147aeb6

Please sign in to comment.