diff options
Diffstat (limited to 'modules')
126 files changed, 18555 insertions, 16285 deletions
diff --git a/modules/SCsub b/modules/SCsub index 9155a53eaf..edfc4ed9c6 100644 --- a/modules/SCsub +++ b/modules/SCsub @@ -1,16 +1,39 @@ #!/usr/bin/env python -Import("env") - import modules_builders import os +Import("env") + env_modules = env.Clone() Export("env_modules") # Header with MODULE_*_ENABLED defines. -env.CommandNoCache("modules_enabled.gen.h", Value(env.module_list), modules_builders.generate_modules_enabled) +env.CommandNoCache( + "modules_enabled.gen.h", + Value(env.module_list), + env.Run( + modules_builders.generate_modules_enabled, + "Generating enabled modules header.", + # NOTE: No need to run in subprocess since this is still executed serially. + subprocess=False, + ), +) + +# Header to be included in `tests/test_main.cpp` to run module-specific tests. +if env["tests"]: + env.CommandNoCache( + "modules_tests.gen.h", + Value(env.module_list), + env.Run( + modules_builders.generate_modules_tests, + "Generating modules tests header.", + # NOTE: No need to run in subprocess since this is still executed serially. + subprocess=False, + ), + ) + env.AlwaysBuild("modules_tests.gen.h") vs_sources = [] # libmodule_<name>.a for each active module. diff --git a/modules/arkit/arkit_interface.h b/modules/arkit/arkit_interface.h index 1044f3cf6f..5a2c50e213 100644 --- a/modules/arkit/arkit_interface.h +++ b/modules/arkit/arkit_interface.h @@ -60,8 +60,8 @@ private: float eye_height, z_near, z_far; Ref<CameraFeed> feed; - int image_width[2]; - int image_height[2]; + size_t image_width[2]; + size_t image_height[2]; Vector<uint8_t> img_data[2]; struct anchor_map { @@ -84,9 +84,9 @@ public: void start_session(); void stop_session(); - bool get_anchor_detection_is_enabled() const; - void set_anchor_detection_is_enabled(bool p_enable); - virtual int get_camera_feed_id(); + bool get_anchor_detection_is_enabled() const override; + void set_anchor_detection_is_enabled(bool p_enable) override; + virtual int get_camera_feed_id() override; bool get_light_estimation_is_enabled() const; void set_light_estimation_is_enabled(bool p_enable); @@ -97,22 +97,22 @@ public: /* while Godot has its own raycast logic this takes ARKits camera into account and hits on any ARAnchor */ Array raycast(Vector2 p_screen_coord); - void notification(int p_what); + virtual void notification(int p_what) override; - virtual StringName get_name() const; - virtual int get_capabilities() const; + virtual StringName get_name() const override; + virtual int get_capabilities() const override; - virtual bool is_initialized() const; - virtual bool initialize(); - virtual void uninitialize(); + virtual bool is_initialized() const override; + virtual bool initialize() override; + virtual void uninitialize() override; - virtual Size2 get_render_targetsize(); - virtual bool is_stereo(); - virtual Transform get_transform_for_eye(XRInterface::Eyes p_eye, const Transform &p_cam_transform); - virtual CameraMatrix get_projection_for_eye(XRInterface::Eyes p_eye, real_t p_aspect, real_t p_z_near, real_t p_z_far); - virtual void commit_for_eye(XRInterface::Eyes p_eye, RID p_render_target, const Rect2 &p_screen_rect); + virtual Size2 get_render_targetsize() override; + virtual bool is_stereo() override; + virtual Transform get_transform_for_eye(XRInterface::Eyes p_eye, const Transform &p_cam_transform) override; + virtual CameraMatrix get_projection_for_eye(XRInterface::Eyes p_eye, real_t p_aspect, real_t p_z_near, real_t p_z_far) override; + virtual void commit_for_eye(XRInterface::Eyes p_eye, RID p_render_target, const Rect2 &p_screen_rect) override; - virtual void process(); + virtual void process() override; // called by delegate (void * because C++ and Obj-C don't always mix, should really change all platform/iphone/*.cpp files to .mm) void _add_or_update_anchor(void *p_anchor); diff --git a/modules/arkit/arkit_interface.mm b/modules/arkit/arkit_interface.mm index 7de824815a..3fb2cc933d 100644 --- a/modules/arkit/arkit_interface.mm +++ b/modules/arkit/arkit_interface.mm @@ -42,7 +42,9 @@ #include "arkit_session_delegate.h" // just a dirty workaround for now, declare these as globals. I'll probably encapsulate ARSession and associated logic into an mm object and change ARKitInterface to a normal cpp object that consumes it. +API_AVAILABLE(ios(11.0)) ARSession *ar_session; + ARKitSessionDelegate *ar_delegate; NSTimeInterval last_timestamp; @@ -55,22 +57,28 @@ void ARKitInterface::start_session() { if (initialized) { print_line("Starting ARKit session"); - Class ARWorldTrackingConfigurationClass = NSClassFromString(@"ARWorldTrackingConfiguration"); - ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfigurationClass new]; + if (@available(iOS 11, *)) { + Class ARWorldTrackingConfigurationClass = NSClassFromString(@"ARWorldTrackingConfiguration"); + ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfigurationClass new]; - configuration.lightEstimationEnabled = light_estimation_is_enabled; - if (plane_detection_is_enabled) { - configuration.planeDetection = ARPlaneDetectionVertical | ARPlaneDetectionHorizontal; - } else { - configuration.planeDetection = 0; - } + configuration.lightEstimationEnabled = light_estimation_is_enabled; + if (plane_detection_is_enabled) { + if (@available(iOS 11.3, *)) { + configuration.planeDetection = ARPlaneDetectionVertical | ARPlaneDetectionHorizontal; + } else { + configuration.planeDetection = ARPlaneDetectionHorizontal; + } + } else { + configuration.planeDetection = 0; + } - // make sure our camera is on - if (feed.is_valid()) { - feed->set_active(true); - } + // make sure our camera is on + if (feed.is_valid()) { + feed->set_active(true); + } - [ar_session runWithConfiguration:configuration]; + [ar_session runWithConfiguration:configuration]; + } } } @@ -84,7 +92,9 @@ void ARKitInterface::stop_session() { feed->set_active(false); } - [ar_session pause]; + if (@available(iOS 11.0, *)) { + [ar_session pause]; + } } } @@ -92,12 +102,12 @@ void ARKitInterface::notification(int p_what) { // TODO, this is not being called, need to find out why, possibly because this is not a node. // in that case we need to find a way to get these notifications! switch (p_what) { - case MainLoop::NOTIFICATION_WM_FOCUS_IN: { + case DisplayServer::WINDOW_EVENT_FOCUS_IN: { print_line("Focus in"); start_session(); }; break; - case MainLoop::NOTIFICATION_WM_FOCUS_OUT: { + case DisplayServer::WINDOW_EVENT_FOCUS_OUT: { print_line("Focus out"); stop_session(); @@ -162,37 +172,42 @@ int ARKitInterface::get_capabilities() const { } Array ARKitInterface::raycast(Vector2 p_screen_coord) { - Array arr; - Size2 screen_size = OS::get_singleton()->get_window_size(); - CGPoint point; - point.x = p_screen_coord.x / screen_size.x; - point.y = p_screen_coord.y / screen_size.y; - - ///@TODO maybe give more options here, for now we're taking just ARAchors into account that were found during plane detection keeping their size into account - NSArray<ARHitTestResult *> *results = [ar_session.currentFrame hittest:point types:ARHitTestResultTypeExistingPlaneUsingExtent]; - - for (ARHitTestResult *result in results) { - Transform transform; - - matrix_float4x4 m44 = result.worldTransform; - transform.basis.elements[0].x = m44.columns[0][0]; - transform.basis.elements[1].x = m44.columns[0][1]; - transform.basis.elements[2].x = m44.columns[0][2]; - transform.basis.elements[0].y = m44.columns[1][0]; - transform.basis.elements[1].y = m44.columns[1][1]; - transform.basis.elements[2].y = m44.columns[1][2]; - transform.basis.elements[0].z = m44.columns[2][0]; - transform.basis.elements[1].z = m44.columns[2][1]; - transform.basis.elements[2].z = m44.columns[2][2]; - transform.origin.x = m44.columns[3][0]; - transform.origin.y = m44.columns[3][1]; - transform.origin.z = m44.columns[3][2]; - - /* important, NOT scaled to world_scale !! */ - arr.push_back(transform); - } + if (@available(iOS 11, *)) { + Array arr; + Size2 screen_size = DisplayServer::get_singleton()->screen_get_size(); + CGPoint point; + point.x = p_screen_coord.x / screen_size.x; + point.y = p_screen_coord.y / screen_size.y; + + ///@TODO maybe give more options here, for now we're taking just ARAchors into account that were found during plane detection keeping their size into account + + NSArray<ARHitTestResult *> *results = [ar_session.currentFrame hitTest:point types:ARHitTestResultTypeExistingPlaneUsingExtent]; + + for (ARHitTestResult *result in results) { + Transform transform; + + matrix_float4x4 m44 = result.worldTransform; + transform.basis.elements[0].x = m44.columns[0][0]; + transform.basis.elements[1].x = m44.columns[0][1]; + transform.basis.elements[2].x = m44.columns[0][2]; + transform.basis.elements[0].y = m44.columns[1][0]; + transform.basis.elements[1].y = m44.columns[1][1]; + transform.basis.elements[2].y = m44.columns[1][2]; + transform.basis.elements[0].z = m44.columns[2][0]; + transform.basis.elements[1].z = m44.columns[2][1]; + transform.basis.elements[2].z = m44.columns[2][2]; + transform.origin.x = m44.columns[3][0]; + transform.origin.y = m44.columns[3][1]; + transform.origin.z = m44.columns[3][2]; + + /* important, NOT scaled to world_scale !! */ + arr.push_back(transform); + } - return arr; + return arr; + } else { + return Array(); + } } void ARKitInterface::_bind_methods() { @@ -221,51 +236,55 @@ bool ARKitInterface::initialize() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, false); - if (!initialized) { - print_line("initializing ARKit"); + if (@available(iOS 11, *)) { + if (!initialized) { + print_line("initializing ARKit"); - // create our ar session and delegate - Class ARSessionClass = NSClassFromString(@"ARSession"); - if (ARSessionClass == Nil) { - void *arkit_handle = dlopen("/System/Library/Frameworks/ARKit.framework/ARKit", RTLD_NOW); - if (arkit_handle) { - ARSessionClass = NSClassFromString(@"ARSession"); - } else { - print_line("ARKit init failed"); - return false; + // create our ar session and delegate + Class ARSessionClass = NSClassFromString(@"ARSession"); + if (ARSessionClass == Nil) { + void *arkit_handle = dlopen("/System/Library/Frameworks/ARKit.framework/ARKit", RTLD_NOW); + if (arkit_handle) { + ARSessionClass = NSClassFromString(@"ARSession"); + } else { + print_line("ARKit init failed"); + return false; + } } - } - ar_session = [ARSessionClass new]; - ar_delegate = [ARKitSessionDelegate new]; - ar_delegate.arkit_interface = this; - ar_session.delegate = ar_delegate; + ar_session = [ARSessionClass new]; + ar_delegate = [ARKitSessionDelegate new]; + ar_delegate.arkit_interface = this; + ar_session.delegate = ar_delegate; - // reset our transform - transform = Transform(); + // reset our transform + transform = Transform(); - // make this our primary interface - xr_server->set_primary_interface(this); + // make this our primary interface + xr_server->set_primary_interface(this); - // make sure we have our feed setup - if (feed.is_null()) { - feed.instance(); - feed->set_name("ARKit"); + // make sure we have our feed setup + if (feed.is_null()) { + feed.instance(); + feed->set_name("ARKit"); - CameraServer *cs = CameraServer::get_singleton(); - if (cs != NULL) { - cs->add_feed(feed); + CameraServer *cs = CameraServer::get_singleton(); + if (cs != NULL) { + cs->add_feed(feed); + } } - } - feed->set_active(true); + feed->set_active(true); - // yeah! - initialized = true; + // yeah! + initialized = true; - // Start our session... - start_session(); - } + // Start our session... + start_session(); + } - return true; + return true; + } else { + return false; + } } void ARKitInterface::uninitialize() { @@ -286,9 +305,12 @@ void ARKitInterface::uninitialize() { remove_all_anchors(); - [ar_session release]; + if (@available(iOS 11.0, *)) { + [ar_session release]; + ar_session = NULL; + } [ar_delegate release]; - ar_session = NULL; + ar_delegate = NULL; initialized = false; session_was_started = false; @@ -296,15 +318,15 @@ void ARKitInterface::uninitialize() { } Size2 ARKitInterface::get_render_targetsize() { - _THREAD_SAFE_METHOD_ + // _THREAD_SAFE_METHOD_ - Size2 target_size = OS::get_singleton()->get_window_size(); + Size2 target_size = DisplayServer::get_singleton()->screen_get_size(); return target_size; } Transform ARKitInterface::get_transform_for_eye(XRInterface::Eyes p_eye, const Transform &p_cam_transform) { - _THREAD_SAFE_METHOD_ + // _THREAD_SAFE_METHOD_ Transform transform_for_eye; @@ -336,7 +358,7 @@ CameraMatrix ARKitInterface::get_projection_for_eye(XRInterface::Eyes p_eye, rea } void ARKitInterface::commit_for_eye(XRInterface::Eyes p_eye, RID p_render_target, const Rect2 &p_screen_rect) { - _THREAD_SAFE_METHOD_ + // _THREAD_SAFE_METHOD_ // We must have a valid render target ERR_FAIL_COND(!p_render_target.is_valid()); @@ -345,15 +367,15 @@ void ARKitInterface::commit_for_eye(XRInterface::Eyes p_eye, RID p_render_target ERR_FAIL_COND(p_screen_rect == Rect2()); // get the size of our screen - Rect2 screen_rect = p_screen_rect; + // Rect2 screen_rect = p_screen_rect; // screen_rect.position.x += screen_rect.size.x; // screen_rect.size.x = -screen_rect.size.x; // screen_rect.position.y += screen_rect.size.y; // screen_rect.size.y = -screen_rect.size.y; - VSG::rasterizer->set_current_render_target(RID()); - VSG::rasterizer->blit_render_target_to_screen(p_render_target, screen_rect, 0); + // VSG::rasterizer->set_current_render_target(RID()); + // VSG::rasterizer->blit_render_target_to_screen(p_render_target, screen_rect, 0); } XRPositionalTracker *ARKitInterface::get_anchor_for_uuid(const unsigned char *p_uuid) { @@ -432,7 +454,7 @@ void ARKitInterface::remove_all_anchors() { } void ARKitInterface::process() { - _THREAD_SAFE_METHOD_ + // _THREAD_SAFE_METHOD_ if (@available(iOS 11.0, *)) { if (initialized) { @@ -443,8 +465,16 @@ void ARKitInterface::process() { last_timestamp = current_frame.timestamp; // get some info about our screen and orientation - Size2 screen_size = OS::get_singleton()->get_window_size(); - UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + Size2 screen_size = DisplayServer::get_singleton()->screen_get_size(); + UIInterfaceOrientation orientation = UIInterfaceOrientationUnknown; + + if (@available(iOS 13, *)) { + orientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + orientation = [[UIApplication sharedApplication] statusBarOrientation]; +#endif + } // Grab our camera image for our backbuffer CVPixelBufferRef pixelBuffer = current_frame.capturedImage; @@ -475,27 +505,27 @@ void ARKitInterface::process() { { // do Y - int new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); - int new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); - int bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + size_t new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + size_t new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); + size_t bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); if ((image_width[0] != new_width) || (image_height[0] != new_height)) { printf("- Camera padding l:%lu r:%lu t:%lu b:%lu\n", extraLeft, extraRight, extraTop, extraBottom); - printf("- Camera Y plane size: %i, %i - %i\n", new_width, new_height, bytes_per_row); + printf("- Camera Y plane size: %lu, %lu - %lu\n", new_width, new_height, bytes_per_row); image_width[0] = new_width; image_height[0] = new_height; img_data[0].resize(new_width * new_height); } - uint8_t *w = img_data[0].write(); + uint8_t *w = img_data[0].ptrw(); if (new_width == bytes_per_row) { - memcpy(w.ptr(), dataY, new_width * new_height); + memcpy(w, dataY, new_width * new_height); } else { - int offset_a = 0; - int offset_b = extraLeft + (extraTop * bytes_per_row); - for (int r = 0; r < new_height; r++) { - memcpy(w.ptr() + offset_a, dataY + offset_b, new_width); + size_t offset_a = 0; + size_t offset_b = extraLeft + (extraTop * bytes_per_row); + for (size_t r = 0; r < new_height; r++) { + memcpy(w + offset_a, dataY + offset_b, new_width); offset_a += new_width; offset_b += bytes_per_row; } @@ -507,26 +537,26 @@ void ARKitInterface::process() { { // do CbCr - int new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1); - int new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); - int bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + size_t new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1); + size_t new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); + size_t bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); if ((image_width[1] != new_width) || (image_height[1] != new_height)) { - printf("- Camera CbCr plane size: %i, %i - %i\n", new_width, new_height, bytes_per_row); + printf("- Camera CbCr plane size: %lu, %lu - %lu\n", new_width, new_height, bytes_per_row); image_width[1] = new_width; image_height[1] = new_height; img_data[1].resize(2 * new_width * new_height); } - uint8_t *w = img_data[1].write(); + uint8_t *w = img_data[1].ptrw(); if ((2 * new_width) == bytes_per_row) { - memcpy(w.ptr(), dataCbCr, 2 * new_width * new_height); + memcpy(w, dataCbCr, 2 * new_width * new_height); } else { - int offset_a = 0; - int offset_b = extraLeft + (extraTop * bytes_per_row); - for (int r = 0; r < new_height; r++) { - memcpy(w.ptr() + offset_a, dataCbCr + offset_b, 2 * new_width); + size_t offset_a = 0; + size_t offset_b = extraLeft + (extraTop * bytes_per_row); + for (size_t r = 0; r < new_height; r++) { + memcpy(w + offset_a, dataCbCr + offset_b, 2 * new_width); offset_a += 2 * new_width; offset_b += bytes_per_row; } @@ -658,69 +688,78 @@ void ARKitInterface::process() { } void ARKitInterface::_add_or_update_anchor(void *p_anchor) { - _THREAD_SAFE_METHOD_ - - ARAnchor *anchor = (ARAnchor *)p_anchor; - - unsigned char uuid[16]; - [anchor.identifier getUUIDBytes:uuid]; - - XRPositionalTracker *tracker = get_anchor_for_uuid(uuid); - if (tracker != NULL) { - // lets update our mesh! (using Arjens code as is for now) - // we should also probably limit how often we do this... + // _THREAD_SAFE_METHOD_ - // can we safely cast this? - ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; - - if (planeAnchor.geometry.triangleCount > 0) { - Ref<SurfaceTool> surftool; - surftool.instance(); - surftool->begin(Mesh::PRIMITIVE_TRIANGLES); + if (@available(iOS 11.0, *)) { + ARAnchor *anchor = (ARAnchor *)p_anchor; + + unsigned char uuid[16]; + [anchor.identifier getUUIDBytes:uuid]; + + XRPositionalTracker *tracker = get_anchor_for_uuid(uuid); + if (tracker != NULL) { + // lets update our mesh! (using Arjens code as is for now) + // we should also probably limit how often we do this... + + // can we safely cast this? + ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; + + if (@available(iOS 11.3, *)) { + if (planeAnchor.geometry.triangleCount > 0) { + Ref<SurfaceTool> surftool; + surftool.instance(); + surftool->begin(Mesh::PRIMITIVE_TRIANGLES); + + for (int j = planeAnchor.geometry.triangleCount * 3 - 1; j >= 0; j--) { + int16_t index = planeAnchor.geometry.triangleIndices[j]; + simd_float3 vrtx = planeAnchor.geometry.vertices[index]; + simd_float2 textcoord = planeAnchor.geometry.textureCoordinates[index]; + surftool->add_uv(Vector2(textcoord[0], textcoord[1])); + surftool->add_color(Color(0.8, 0.8, 0.8)); + surftool->add_vertex(Vector3(vrtx[0], vrtx[1], vrtx[2])); + } - for (int j = planeAnchor.geometry.triangleCount * 3 - 1; j >= 0; j--) { - int16_t index = planeAnchor.geometry.triangleIndices[j]; - simd_float3 vrtx = planeAnchor.geometry.vertices[index]; - simd_float2 textcoord = planeAnchor.geometry.textureCoordinates[index]; - surftool->add_uv(Vector2(textcoord[0], textcoord[1])); - surftool->add_color(Color(0.8, 0.8, 0.8)); - surftool->add_vertex(Vector3(vrtx[0], vrtx[1], vrtx[2])); + surftool->generate_normals(); + tracker->set_mesh(surftool->commit()); + } else { + Ref<Mesh> nomesh; + tracker->set_mesh(nomesh); + } + } else { + Ref<Mesh> nomesh; + tracker->set_mesh(nomesh); } - surftool->generate_normals(); - tracker->set_mesh(surftool->commit()); - } else { - Ref<Mesh> nomesh; - tracker->set_mesh(nomesh); + // Note, this also contains a scale factor which gives us an idea of the size of the anchor + // We may extract that in our XRAnchor class + Basis b; + matrix_float4x4 m44 = anchor.transform; + b.elements[0].x = m44.columns[0][0]; + b.elements[1].x = m44.columns[0][1]; + b.elements[2].x = m44.columns[0][2]; + b.elements[0].y = m44.columns[1][0]; + b.elements[1].y = m44.columns[1][1]; + b.elements[2].y = m44.columns[1][2]; + b.elements[0].z = m44.columns[2][0]; + b.elements[1].z = m44.columns[2][1]; + b.elements[2].z = m44.columns[2][2]; + tracker->set_orientation(b); + tracker->set_rw_position(Vector3(m44.columns[3][0], m44.columns[3][1], m44.columns[3][2])); } - - // Note, this also contains a scale factor which gives us an idea of the size of the anchor - // We may extract that in our XRAnchor class - Basis b; - matrix_float4x4 m44 = anchor.transform; - b.elements[0].x = m44.columns[0][0]; - b.elements[1].x = m44.columns[0][1]; - b.elements[2].x = m44.columns[0][2]; - b.elements[0].y = m44.columns[1][0]; - b.elements[1].y = m44.columns[1][1]; - b.elements[2].y = m44.columns[1][2]; - b.elements[0].z = m44.columns[2][0]; - b.elements[1].z = m44.columns[2][1]; - b.elements[2].z = m44.columns[2][2]; - tracker->set_orientation(b); - tracker->set_rw_position(Vector3(m44.columns[3][0], m44.columns[3][1], m44.columns[3][2])); } } void ARKitInterface::_remove_anchor(void *p_anchor) { - _THREAD_SAFE_METHOD_ + // _THREAD_SAFE_METHOD_ - ARAnchor *anchor = (ARAnchor *)p_anchor; + if (@available(iOS 11.0, *)) { + ARAnchor *anchor = (ARAnchor *)p_anchor; - unsigned char uuid[16]; - [anchor.identifier getUUIDBytes:uuid]; + unsigned char uuid[16]; + [anchor.identifier getUUIDBytes:uuid]; - remove_anchor_for_uuid(uuid); + remove_anchor_for_uuid(uuid); + } } ARKitInterface::ARKitInterface() { @@ -728,7 +767,9 @@ ARKitInterface::ARKitInterface() { session_was_started = false; plane_detection_is_enabled = false; light_estimation_is_enabled = false; - ar_session = NULL; + if (@available(iOS 11.0, *)) { + ar_session = NULL; + } z_near = 0.01; z_far = 1000.0; projection.set_perspective(60.0, 1.0, z_near, z_far, false); diff --git a/modules/arkit/arkit_session_delegate.h b/modules/arkit/arkit_session_delegate.h index 158b80a60a..df98bf506e 100644 --- a/modules/arkit/arkit_session_delegate.h +++ b/modules/arkit/arkit_session_delegate.h @@ -42,9 +42,9 @@ class ARKitInterface; @property(nonatomic) ARKitInterface *arkit_interface; -- (void)session:(ARSession *)session didAddAnchors:(NSArray<ARAnchor *> *)anchors; -- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<ARAnchor *> *)anchors; -- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<ARAnchor *> *)anchors; +- (void)session:(ARSession *)session didAddAnchors:(NSArray<ARAnchor *> *)anchors API_AVAILABLE(ios(11.0)); +- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<ARAnchor *> *)anchors API_AVAILABLE(ios(11.0)); +- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<ARAnchor *> *)anchors API_AVAILABLE(ios(11.0)); @end #endif /* !ARKIT_SESSION_DELEGATE_H */ diff --git a/modules/assimp/editor_scene_importer_assimp.cpp b/modules/assimp/editor_scene_importer_assimp.cpp index aedc4b690a..e5becfd559 100644 --- a/modules/assimp/editor_scene_importer_assimp.cpp +++ b/modules/assimp/editor_scene_importer_assimp.cpp @@ -441,7 +441,6 @@ EditorSceneImporterAssimp::_generate_scene(const String &p_path, aiScene *scene, Transform pform = AssimpUtils::assimp_matrix_transform(bone->mNode->mTransformation); skeleton->add_bone(bone_name); skeleton->set_bone_rest(boneIdx, pform); - skeleton->set_bone_pose(boneIdx, pform); if (parent_node != nullptr) { int parent_bone_id = skeleton->find_bone(AssimpUtils::get_anim_string_from_assimp(parent_node->mName)); @@ -612,7 +611,7 @@ void EditorSceneImporterAssimp::_insert_animation_track(ImportState &scene, cons xform.basis.set_quat_scale(rot, scale); xform.origin = pos; - xform = skeleton->get_bone_pose(skeleton_bone).inverse() * xform; + xform = skeleton->get_bone_rest(skeleton_bone).inverse() * xform; rot = xform.basis.get_rotation_quat(); rot.normalize(); diff --git a/modules/bmp/image_loader_bmp.cpp b/modules/bmp/image_loader_bmp.cpp index ac4e534983..757afeb9e3 100644 --- a/modules/bmp/image_loader_bmp.cpp +++ b/modules/bmp/image_loader_bmp.cpp @@ -51,16 +51,20 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, if (bits_per_pixel == 1) { // Requires bit unpacking... - ERR_FAIL_COND_V(width % 8 != 0, ERR_UNAVAILABLE); - ERR_FAIL_COND_V(height % 8 != 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V_MSG(width % 8 != 0, ERR_UNAVAILABLE, + vformat("1-bpp BMP images must have a width that is a multiple of 8, but the imported BMP is %d pixels wide.", int(width))); + ERR_FAIL_COND_V_MSG(height % 8 != 0, ERR_UNAVAILABLE, + vformat("1-bpp BMP images must have a height that is a multiple of 8, but the imported BMP is %d pixels tall.", int(height))); } else if (bits_per_pixel == 4) { // Requires bit unpacking... - ERR_FAIL_COND_V(width % 2 != 0, ERR_UNAVAILABLE); - ERR_FAIL_COND_V(height % 2 != 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V_MSG(width % 2 != 0, ERR_UNAVAILABLE, + vformat("4-bpp BMP images must have a width that is a multiple of 2, but the imported BMP is %d pixels wide.", int(width))); + ERR_FAIL_COND_V_MSG(height % 2 != 0, ERR_UNAVAILABLE, + vformat("4-bpp BMP images must have a height that is a multiple of 2, but the imported BMP is %d pixels tall.", int(height))); } else if (bits_per_pixel == 16) { - ERR_FAIL_V(ERR_UNAVAILABLE); + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "16-bpp BMP images are not supported."); } // Image data (might be indexed) @@ -72,7 +76,7 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, } else { // color data_len = width * height * 4; } - ERR_FAIL_COND_V(data_len == 0, ERR_BUG); + ERR_FAIL_COND_V_MSG(data_len == 0, ERR_BUG, "Couldn't parse the BMP image data."); err = data.resize(data_len); uint8_t *data_w = data.ptrw(); @@ -215,13 +219,15 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, // Info Header bmp_header.bmp_info_header.bmp_header_size = f->get_32(); - ERR_FAIL_COND_V(bmp_header.bmp_info_header.bmp_header_size < BITMAP_INFO_HEADER_MIN_SIZE, ERR_FILE_CORRUPT); + ERR_FAIL_COND_V_MSG(bmp_header.bmp_info_header.bmp_header_size < BITMAP_INFO_HEADER_MIN_SIZE, ERR_FILE_CORRUPT, + vformat("Couldn't parse the BMP info header. The file is likely corrupt: %s", f->get_path())); bmp_header.bmp_info_header.bmp_width = f->get_32(); bmp_header.bmp_info_header.bmp_height = f->get_32(); bmp_header.bmp_info_header.bmp_planes = f->get_16(); - ERR_FAIL_COND_V(bmp_header.bmp_info_header.bmp_planes != 1, ERR_FILE_CORRUPT); + ERR_FAIL_COND_V_MSG(bmp_header.bmp_info_header.bmp_planes != 1, ERR_FILE_CORRUPT, + vformat("Couldn't parse the BMP planes. The file is likely corrupt: %s", f->get_path())); bmp_header.bmp_info_header.bmp_bit_count = f->get_16(); bmp_header.bmp_info_header.bmp_compression = f->get_32(); @@ -236,10 +242,10 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, case BI_RLE4: case BI_CMYKRLE8: case BI_CMYKRLE4: { - // Stop parsing - String bmp_path = f->get_path(); + // Stop parsing. f->close(); - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Compressed BMP files are not supported: " + bmp_path + "."); + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, + vformat("Compressed BMP files are not supported: %s", f->get_path())); } break; } // Don't rely on sizeof(bmp_file_header) as structure padding @@ -255,7 +261,8 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, if (bmp_header.bmp_info_header.bmp_bit_count <= 8) { // Support 256 colors max color_table_size = 1 << bmp_header.bmp_info_header.bmp_bit_count; - ERR_FAIL_COND_V(color_table_size == 0, ERR_BUG); + ERR_FAIL_COND_V_MSG(color_table_size == 0, ERR_BUG, + vformat("Couldn't parse the BMP color table: %s", f->get_path())); } Vector<uint8_t> bmp_color_table; diff --git a/modules/bullet/area_bullet.cpp b/modules/bullet/area_bullet.cpp index 01f068780d..b35019bea3 100644 --- a/modules/bullet/area_bullet.cpp +++ b/modules/bullet/area_bullet.cpp @@ -65,14 +65,11 @@ AreaBullet::~AreaBullet() { } void AreaBullet::dispatch_callbacks() { - if (!isScratched) { - return; - } - isScratched = false; + RigidCollisionObjectBullet::dispatch_callbacks(); // Reverse order because I've to remove EXIT objects for (int i = overlappingObjects.size() - 1; 0 <= i; --i) { - OverlappingObjectData &otherObj = overlappingObjects.write[i]; + OverlappingObjectData &otherObj = overlappingObjects[i]; switch (otherObj.state) { case OVERLAP_STATE_ENTER: @@ -112,10 +109,9 @@ void AreaBullet::call_event(CollisionObjectBullet *p_otherObject, PhysicsServer3 } void AreaBullet::scratch() { - if (isScratched) { - return; + if (space != nullptr) { + space->add_to_pre_flush_queue(this); } - isScratched = true; } void AreaBullet::clear_overlaps(bool p_notify) { @@ -173,9 +169,9 @@ void AreaBullet::do_reload_body() { void AreaBullet::set_space(SpaceBullet *p_space) { // Clear the old space if there is one + if (space) { - overlappingObjects.clear(); - isScratched = false; + clear_overlaps(false); // Remove this object form the physics world space->unregister_collision_object(this); @@ -187,10 +183,11 @@ void AreaBullet::set_space(SpaceBullet *p_space) { if (space) { space->register_collision_object(this); reload_body(); + scratch(); } } -void AreaBullet::on_collision_filters_change() { +void AreaBullet::do_reload_collision_filters() { if (space) { space->reload_collision_filters(this); } @@ -204,13 +201,13 @@ void AreaBullet::add_overlap(CollisionObjectBullet *p_otherObject) { void AreaBullet::put_overlap_as_exit(int p_index) { scratch(); - overlappingObjects.write[p_index].state = OVERLAP_STATE_EXIT; + overlappingObjects[p_index].state = OVERLAP_STATE_EXIT; } void AreaBullet::put_overlap_as_inside(int p_index) { // This check is required to be sure this body was inside if (OVERLAP_STATE_DIRTY == overlappingObjects[p_index].state) { - overlappingObjects.write[p_index].state = OVERLAP_STATE_INSIDE; + overlappingObjects[p_index].state = OVERLAP_STATE_INSIDE; } } diff --git a/modules/bullet/area_bullet.h b/modules/bullet/area_bullet.h index 12272092f7..51fbc1f71d 100644 --- a/modules/bullet/area_bullet.h +++ b/modules/bullet/area_bullet.h @@ -32,7 +32,7 @@ #define AREABULLET_H #include "collision_object_bullet.h" -#include "core/vector.h" +#include "core/local_vector.h" #include "servers/physics_server_3d.h" #include "space_bullet.h" @@ -83,7 +83,7 @@ private: Variant *call_event_res_ptr[5]; btGhostObject *btGhost; - Vector<OverlappingObjectData> overlappingObjects; + LocalVector<OverlappingObjectData> overlappingObjects; bool monitorable = true; PhysicsServer3D::AreaSpaceOverrideMode spOv_mode = PhysicsServer3D::AREA_SPACE_OVERRIDE_DISABLED; @@ -96,8 +96,6 @@ private: real_t spOv_angularDump = 0.1; int spOv_priority = 0; - bool isScratched = false; - InOutEventCallback eventsCallbacks[2]; public: @@ -139,11 +137,11 @@ public: _FORCE_INLINE_ void set_spOv_priority(int p_priority) { spOv_priority = p_priority; } _FORCE_INLINE_ int get_spOv_priority() { return spOv_priority; } - virtual void main_shape_changed(); - virtual void do_reload_body(); - virtual void set_space(SpaceBullet *p_space); + virtual void main_shape_changed() override; + virtual void do_reload_body() override; + virtual void set_space(SpaceBullet *p_space) override; - virtual void dispatch_callbacks(); + virtual void dispatch_callbacks() override; void call_event(CollisionObjectBullet *p_otherObject, PhysicsServer3D::AreaBodyStatus p_status); void set_on_state_change(ObjectID p_id, const StringName &p_method, const Variant &p_udata = Variant()); void scratch(); @@ -152,9 +150,9 @@ public: // Dispatch the callbacks and removes from overlapping list void remove_overlap(CollisionObjectBullet *p_object, bool p_notify); - virtual void on_collision_filters_change(); - virtual void on_collision_checker_start() {} - virtual void on_collision_checker_end() { isTransformChanged = false; } + virtual void do_reload_collision_filters() override; + virtual void on_collision_checker_start() override {} + virtual void on_collision_checker_end() override { isTransformChanged = false; } void add_overlap(CollisionObjectBullet *p_otherObject); void put_overlap_as_exit(int p_index); @@ -166,8 +164,8 @@ public: void set_event_callback(Type p_callbackObjectType, ObjectID p_id, const StringName &p_method); bool has_event_callback(Type p_callbackObjectType); - virtual void on_enter_area(AreaBullet *p_area); - virtual void on_exit_area(AreaBullet *p_area); + virtual void on_enter_area(AreaBullet *p_area) override; + virtual void on_exit_area(AreaBullet *p_area) override; }; #endif diff --git a/modules/bullet/bullet_physics_server.h b/modules/bullet/bullet_physics_server.h index 6078babaf8..eb95120f74 100644 --- a/modules/bullet/bullet_physics_server.h +++ b/modules/bullet/bullet_physics_server.h @@ -52,7 +52,7 @@ class BulletPhysicsServer3D : public PhysicsServer3D { bool active = true; char active_spaces_count = 0; - Vector<SpaceBullet *> active_spaces; + LocalVector<SpaceBullet *> active_spaces; mutable RID_PtrOwner<SpaceBullet> space_owner; mutable RID_PtrOwner<ShapeBullet> shape_owner; diff --git a/modules/bullet/collision_object_bullet.cpp b/modules/bullet/collision_object_bullet.cpp index dd208965bd..660e9afc5e 100644 --- a/modules/bullet/collision_object_bullet.cpp +++ b/modules/bullet/collision_object_bullet.cpp @@ -165,11 +165,20 @@ bool CollisionObjectBullet::has_collision_exception(const CollisionObjectBullet return !bt_collision_object->checkCollideWith(p_otherCollisionObject->bt_collision_object); } -void CollisionObjectBullet::prepare_object_for_dispatch() { - if (need_body_reload) { +void CollisionObjectBullet::reload_body() { + needs_body_reload = true; +} + +void CollisionObjectBullet::dispatch_callbacks() {} + +void CollisionObjectBullet::pre_process() { + if (needs_body_reload) { do_reload_body(); - need_body_reload = false; + } else if (needs_collision_filters_reload) { + do_reload_collision_filters(); } + needs_body_reload = false; + needs_collision_filters_reload = false; } void CollisionObjectBullet::set_collision_enabled(bool p_enabled) { @@ -245,7 +254,7 @@ void RigidCollisionObjectBullet::add_shape(ShapeBullet *p_shape, const Transform } void RigidCollisionObjectBullet::set_shape(int p_index, ShapeBullet *p_shape) { - ShapeWrapper &shp = shapes.write[p_index]; + ShapeWrapper &shp = shapes[p_index]; shp.shape->remove_owner(this); p_shape->add_owner(this); shp.shape = p_shape; @@ -307,7 +316,7 @@ void RigidCollisionObjectBullet::remove_all_shapes(bool p_permanentlyFromThisBod void RigidCollisionObjectBullet::set_shape_transform(int p_index, const Transform &p_transform) { ERR_FAIL_INDEX(p_index, get_shape_count()); - shapes.write[p_index].set_transform(p_transform); + shapes[p_index].set_transform(p_transform); shape_changed(p_index); } @@ -325,7 +334,7 @@ void RigidCollisionObjectBullet::set_shape_disabled(int p_index, bool p_disabled if (shapes[p_index].active != p_disabled) { return; } - shapes.write[p_index].active = !p_disabled; + shapes[p_index].active = !p_disabled; shape_changed(p_index); } @@ -333,16 +342,16 @@ bool RigidCollisionObjectBullet::is_shape_disabled(int p_index) { return !shapes[p_index].active; } -void RigidCollisionObjectBullet::prepare_object_for_dispatch() { +void RigidCollisionObjectBullet::pre_process() { if (need_shape_reload) { do_reload_shapes(); need_shape_reload = false; } - CollisionObjectBullet::prepare_object_for_dispatch(); + CollisionObjectBullet::pre_process(); } void RigidCollisionObjectBullet::shape_changed(int p_shape_index) { - ShapeWrapper &shp = shapes.write[p_shape_index]; + ShapeWrapper &shp = shapes[p_shape_index]; if (shp.bt_shape == mainShape) { mainShape = nullptr; } @@ -363,12 +372,11 @@ void RigidCollisionObjectBullet::do_reload_shapes() { mainShape = nullptr; const int shape_count = shapes.size(); - ShapeWrapper *shapes_ptr = shapes.ptrw(); // Reset all shapes if required if (force_shape_reset) { for (int i(0); i < shape_count; ++i) { - shapes_ptr[i].release_bt_shape(); + shapes[i].release_bt_shape(); } force_shape_reset = false; } @@ -377,10 +385,10 @@ void RigidCollisionObjectBullet::do_reload_shapes() { if (1 == shape_count) { // Is it possible to optimize by not using compound? - btTransform transform = shapes_ptr[0].get_adjusted_transform(); + btTransform transform = shapes[0].get_adjusted_transform(); if (transform.getOrigin().isZero() && transform.getBasis() == transform.getBasis().getIdentity()) { - shapes_ptr[0].claim_bt_shape(body_scale); - mainShape = shapes_ptr[0].bt_shape; + shapes[0].claim_bt_shape(body_scale); + mainShape = shapes[0].bt_shape; main_shape_changed(); // Nothing more to do return; @@ -391,10 +399,10 @@ void RigidCollisionObjectBullet::do_reload_shapes() { btCompoundShape *compoundShape = bulletnew(btCompoundShape(enableDynamicAabbTree, shape_count)); for (int i(0); i < shape_count; ++i) { - shapes_ptr[i].claim_bt_shape(body_scale); - btTransform scaled_shape_transform(shapes_ptr[i].get_adjusted_transform()); + shapes[i].claim_bt_shape(body_scale); + btTransform scaled_shape_transform(shapes[i].get_adjusted_transform()); scaled_shape_transform.getOrigin() *= body_scale; - compoundShape->addChildShape(scaled_shape_transform, shapes_ptr[i].bt_shape); + compoundShape->addChildShape(scaled_shape_transform, shapes[i].bt_shape); } compoundShape->recalculateLocalAabb(); @@ -408,7 +416,7 @@ void RigidCollisionObjectBullet::body_scale_changed() { } void RigidCollisionObjectBullet::internal_shape_destroy(int p_index, bool p_permanentlyFromThisBody) { - ShapeWrapper &shp = shapes.write[p_index]; + ShapeWrapper &shp = shapes[p_index]; shp.shape->remove_owner(this, p_permanentlyFromThisBody); if (shp.bt_shape == mainShape) { mainShape = nullptr; diff --git a/modules/bullet/collision_object_bullet.h b/modules/bullet/collision_object_bullet.h index ac74661f24..920d80af23 100644 --- a/modules/bullet/collision_object_bullet.h +++ b/modules/bullet/collision_object_bullet.h @@ -31,6 +31,7 @@ #ifndef COLLISION_OBJECT_BULLET_H #define COLLISION_OBJECT_BULLET_H +#include "core/local_vector.h" #include "core/math/transform.h" #include "core/math/vector3.h" #include "core/object.h" @@ -126,16 +127,18 @@ protected: VSet<RID> exceptions; - bool need_body_reload = true; + bool needs_body_reload = true; + bool needs_collision_filters_reload = true; /// This array is used to know all areas where this Object is overlapped in /// New area is added when overlap with new area (AreaBullet::addOverlap), then is removed when it exit (CollisionObjectBullet::onExitArea) /// This array is used mainly to know which area hold the pointer of this object - Vector<AreaBullet *> areasOverlapped; + LocalVector<AreaBullet *> areasOverlapped; bool isTransformChanged = false; public: bool is_in_world = false; + bool is_in_flush_queue = false; public: CollisionObjectBullet(Type p_type); @@ -171,7 +174,7 @@ public: _FORCE_INLINE_ void set_collision_layer(uint32_t p_layer) { if (collisionLayer != p_layer) { collisionLayer = p_layer; - on_collision_filters_change(); + needs_collision_filters_reload = true; } } _FORCE_INLINE_ uint32_t get_collision_layer() const { return collisionLayer; } @@ -179,24 +182,23 @@ public: _FORCE_INLINE_ void set_collision_mask(uint32_t p_mask) { if (collisionMask != p_mask) { collisionMask = p_mask; - on_collision_filters_change(); + needs_collision_filters_reload = true; } } _FORCE_INLINE_ uint32_t get_collision_mask() const { return collisionMask; } - virtual void on_collision_filters_change() = 0; + virtual void do_reload_collision_filters() = 0; _FORCE_INLINE_ bool test_collision_mask(CollisionObjectBullet *p_other) const { return collisionLayer & p_other->collisionMask || p_other->collisionLayer & collisionMask; } bool need_reload_body() const { - return need_body_reload; + return needs_body_reload; } - void reload_body() { - need_body_reload = true; - } + void reload_body(); + virtual void do_reload_body() = 0; virtual void set_space(SpaceBullet *p_space) = 0; _FORCE_INLINE_ SpaceBullet *get_space() const { return space; } @@ -204,8 +206,8 @@ public: virtual void on_collision_checker_start() = 0; virtual void on_collision_checker_end() = 0; - virtual void prepare_object_for_dispatch(); - virtual void dispatch_callbacks() = 0; + virtual void dispatch_callbacks(); + virtual void pre_process(); void set_collision_enabled(bool p_enabled); bool is_collisions_response_enabled(); @@ -229,7 +231,7 @@ public: class RigidCollisionObjectBullet : public CollisionObjectBullet, public ShapeOwnerBullet { protected: btCollisionShape *mainShape = nullptr; - Vector<ShapeWrapper> shapes; + LocalVector<ShapeWrapper> shapes; bool need_shape_reload = true; public: @@ -237,7 +239,7 @@ public: CollisionObjectBullet(p_type) {} ~RigidCollisionObjectBullet(); - _FORCE_INLINE_ const Vector<ShapeWrapper> &get_shapes_wrappers() const { return shapes; } + _FORCE_INLINE_ const LocalVector<ShapeWrapper> &get_shapes_wrappers() const { return shapes; } _FORCE_INLINE_ btCollisionShape *get_main_shape() const { return mainShape; } @@ -248,9 +250,9 @@ public: ShapeBullet *get_shape(int p_index) const; btCollisionShape *get_bt_shape(int p_index) const; - int find_shape(ShapeBullet *p_shape) const; + virtual int find_shape(ShapeBullet *p_shape) const override; - virtual void remove_shape_full(ShapeBullet *p_shape); + virtual void remove_shape_full(ShapeBullet *p_shape) override; void remove_shape_full(int p_index); void remove_all_shapes(bool p_permanentlyFromThisBody = false, bool p_force_not_reload = false); @@ -262,15 +264,15 @@ public: void set_shape_disabled(int p_index, bool p_disabled); bool is_shape_disabled(int p_index); - virtual void prepare_object_for_dispatch(); + virtual void pre_process() override; - virtual void shape_changed(int p_shape_index); - void reload_shapes(); + virtual void shape_changed(int p_shape_index) override; + virtual void reload_shapes() override; bool need_reload_shapes() const { return need_shape_reload; } virtual void do_reload_shapes(); virtual void main_shape_changed() = 0; - virtual void body_scale_changed(); + virtual void body_scale_changed() override; private: void internal_shape_destroy(int p_index, bool p_permanentlyFromThisBody = false); diff --git a/modules/bullet/godot_result_callbacks.cpp b/modules/bullet/godot_result_callbacks.cpp index e1f950dad1..f82648d6ff 100644 --- a/modules/bullet/godot_result_callbacks.cpp +++ b/modules/bullet/godot_result_callbacks.cpp @@ -57,7 +57,7 @@ bool GodotFilterCallback::needBroadphaseCollision(btBroadphaseProxy *proxy0, btB bool GodotClosestRayResultCallback::needsCollision(btBroadphaseProxy *proxy0) const { const bool needs = GodotFilterCallback::test_collision_filters(m_collisionFilterGroup, m_collisionFilterMask, proxy0->m_collisionFilterGroup, proxy0->m_collisionFilterMask); - if (m_pickRay || needs) { + if (needs) { btCollisionObject *btObj = static_cast<btCollisionObject *>(proxy0->m_clientObject); CollisionObjectBullet *gObj = static_cast<CollisionObjectBullet *>(btObj->getUserPointer()); diff --git a/modules/bullet/rigid_body_bullet.cpp b/modules/bullet/rigid_body_bullet.cpp index 8ff27cda30..32c3240a35 100644 --- a/modules/bullet/rigid_body_bullet.cpp +++ b/modules/bullet/rigid_body_bullet.cpp @@ -51,9 +51,7 @@ BulletPhysicsDirectBodyState3D *BulletPhysicsDirectBodyState3D::singleton = nullptr; Vector3 BulletPhysicsDirectBodyState3D::get_total_gravity() const { - Vector3 gVec; - B_TO_G(body->btBody->getGravity(), gVec); - return gVec; + return body->total_gravity; } float BulletPhysicsDirectBodyState3D::get_total_angular_damp() const { @@ -183,7 +181,7 @@ int BulletPhysicsDirectBodyState3D::get_contact_collider_shape(int p_contact_idx } Vector3 BulletPhysicsDirectBodyState3D::get_contact_collider_velocity_at_position(int p_contact_idx) const { - RigidBodyBullet::CollisionData &colDat = body->collisions.write[p_contact_idx]; + RigidBodyBullet::CollisionData &colDat = body->collisions[p_contact_idx]; btVector3 hitLocation; G_TO_B(colDat.hitLocalLocation, hitLocation); @@ -213,7 +211,7 @@ void RigidBodyBullet::KinematicUtilities::setSafeMargin(btScalar p_margin) { } void RigidBodyBullet::KinematicUtilities::copyAllOwnerShapes() { - const Vector<CollisionObjectBullet::ShapeWrapper> &shapes_wrappers(owner->get_shapes_wrappers()); + const LocalVector<CollisionObjectBullet::ShapeWrapper> &shapes_wrappers(owner->get_shapes_wrappers()); const int shapes_count = shapes_wrappers.size(); just_delete_shapes(shapes_count); @@ -228,8 +226,8 @@ void RigidBodyBullet::KinematicUtilities::copyAllOwnerShapes() { continue; } - shapes.write[i].transform = shape_wrapper->transform; - shapes.write[i].transform.getOrigin() *= owner_scale; + shapes[i].transform = shape_wrapper->transform; + shapes[i].transform.getOrigin() *= owner_scale; switch (shape_wrapper->shape->get_type()) { case PhysicsServer3D::SHAPE_SPHERE: case PhysicsServer3D::SHAPE_BOX: @@ -237,11 +235,11 @@ void RigidBodyBullet::KinematicUtilities::copyAllOwnerShapes() { case PhysicsServer3D::SHAPE_CYLINDER: case PhysicsServer3D::SHAPE_CONVEX_POLYGON: case PhysicsServer3D::SHAPE_RAY: { - shapes.write[i].shape = static_cast<btConvexShape *>(shape_wrapper->shape->internal_create_bt_shape(owner_scale * shape_wrapper->scale, safe_margin)); + shapes[i].shape = static_cast<btConvexShape *>(shape_wrapper->shape->internal_create_bt_shape(owner_scale * shape_wrapper->scale, safe_margin)); } break; default: WARN_PRINT("This shape is not supported for kinematic collision."); - shapes.write[i].shape = nullptr; + shapes[i].shape = nullptr; } } } @@ -249,7 +247,7 @@ void RigidBodyBullet::KinematicUtilities::copyAllOwnerShapes() { void RigidBodyBullet::KinematicUtilities::just_delete_shapes(int new_size) { for (int i = shapes.size() - 1; 0 <= i; --i) { if (shapes[i].shape) { - bulletdelete(shapes.write[i].shape); + bulletdelete(shapes[i].shape); } } shapes.resize(new_size); @@ -271,8 +269,8 @@ RigidBodyBullet::RigidBodyBullet() : reload_axis_lock(); areasWhereIam.resize(maxAreasWhereIam); - for (int i = areasWhereIam.size() - 1; 0 <= i; --i) { - areasWhereIam.write[i] = nullptr; + for (uint32_t i = 0; i < areasWhereIam.size(); i += 1) { + areasWhereIam[i] = nullptr; } btBody->setSleepingThresholds(0.2, 0.2); @@ -320,6 +318,7 @@ void RigidBodyBullet::set_space(SpaceBullet *p_space) { // Clear the old space if there is one if (space) { can_integrate_forces = false; + isScratchedSpaceOverrideModificator = false; // Remove all eventual constraints assert_no_constraints(); @@ -334,16 +333,15 @@ void RigidBodyBullet::set_space(SpaceBullet *p_space) { if (space) { space->register_collision_object(this); reload_body(); + space->add_to_flush_queue(this); } } void RigidBodyBullet::dispatch_callbacks() { + RigidCollisionObjectBullet::dispatch_callbacks(); + /// The check isFirstTransformChanged is necessary in order to call integrated forces only when the first transform is sent if ((btBody->isKinematicObject() || btBody->isActive() || previousActiveState != btBody->isActive()) && force_integration_callback && can_integrate_forces) { - if (omit_forces_integration) { - btBody->clearForces(); - } - BulletPhysicsDirectBodyState3D *bodyDirect = BulletPhysicsDirectBodyState3D::get_singleton(this); Variant variantBodyDirect = bodyDirect; @@ -361,16 +359,22 @@ void RigidBodyBullet::dispatch_callbacks() { } } + previousActiveState = btBody->isActive(); +} + +void RigidBodyBullet::pre_process() { + RigidCollisionObjectBullet::pre_process(); + if (isScratchedSpaceOverrideModificator || 0 < countGravityPointSpaces) { isScratchedSpaceOverrideModificator = false; reload_space_override_modificator(); } - /// Lock axis - btBody->setLinearVelocity(btBody->getLinearVelocity() * btBody->getLinearFactor()); - btBody->setAngularVelocity(btBody->getAngularVelocity() * btBody->getAngularFactor()); - - previousActiveState = btBody->isActive(); + if (is_active()) { + /// Lock axis + btBody->setLinearVelocity(btBody->getLinearVelocity() * btBody->getLinearFactor()); + btBody->setAngularVelocity(btBody->getAngularVelocity() * btBody->getAngularFactor()); + } } void RigidBodyBullet::set_force_integration_callback(ObjectID p_id, const StringName &p_method, const Variant &p_udata) { @@ -391,7 +395,7 @@ void RigidBodyBullet::scratch_space_override_modificator() { isScratchedSpaceOverrideModificator = true; } -void RigidBodyBullet::on_collision_filters_change() { +void RigidBodyBullet::do_reload_collision_filters() { if (space) { space->reload_collision_filters(this); } @@ -404,14 +408,15 @@ void RigidBodyBullet::on_collision_checker_start() { collisionsCount = 0; // Swap array - Vector<RigidBodyBullet *> *s = prev_collision_traces; - prev_collision_traces = curr_collision_traces; - curr_collision_traces = s; + SWAP(prev_collision_traces, curr_collision_traces); } void RigidBodyBullet::on_collision_checker_end() { // Always true if active and not a static or kinematic body isTransformChanged = btBody->isActive() && !btBody->isStaticOrKinematicObject(); + if (isTransformChanged && space != nullptr) { + space->add_to_flush_queue(this); + } } bool RigidBodyBullet::add_collision_object(RigidBodyBullet *p_otherObject, const Vector3 &p_hitWorldLocation, const Vector3 &p_hitLocalLocation, const Vector3 &p_hitNormal, const float &p_appliedImpulse, int p_other_shape_index, int p_local_shape_index) { @@ -419,7 +424,7 @@ bool RigidBodyBullet::add_collision_object(RigidBodyBullet *p_otherObject, const return false; } - CollisionData &cd = collisions.write[collisionsCount]; + CollisionData &cd = collisions[collisionsCount]; cd.hitLocalLocation = p_hitLocalLocation; cd.otherObject = p_otherObject; cd.hitWorldLocation = p_hitWorldLocation; @@ -428,7 +433,7 @@ bool RigidBodyBullet::add_collision_object(RigidBodyBullet *p_otherObject, const cd.other_object_shape = p_other_shape_index; cd.local_shape = p_local_shape_index; - curr_collision_traces->write[collisionsCount] = p_otherObject; + (*curr_collision_traces)[collisionsCount] = p_otherObject; ++collisionsCount; return true; @@ -463,6 +468,7 @@ bool RigidBodyBullet::is_active() const { void RigidBodyBullet::set_omit_forces_integration(bool p_omit) { omit_forces_integration = p_omit; + scratch_space_override_modificator(); } void RigidBodyBullet::set_param(PhysicsServer3D::BodyParameter p_param, real_t p_value) { @@ -838,15 +844,15 @@ void RigidBodyBullet::on_enter_area(AreaBullet *p_area) { for (int i = 0; i < areaWhereIamCount; ++i) { if (nullptr == areasWhereIam[i]) { // This area has the highest priority - areasWhereIam.write[i] = p_area; + areasWhereIam[i] = p_area; break; } else { if (areasWhereIam[i]->get_spOv_priority() > p_area->get_spOv_priority()) { // The position was found, just shift all elements for (int j = i; j < areaWhereIamCount; ++j) { - areasWhereIam.write[j + 1] = areasWhereIam[j]; + areasWhereIam[j + 1] = areasWhereIam[j]; } - areasWhereIam.write[i] = p_area; + areasWhereIam[i] = p_area; break; } } @@ -870,7 +876,7 @@ void RigidBodyBullet::on_exit_area(AreaBullet *p_area) { if (p_area == areasWhereIam[i]) { // The area was found, just shift down all elements for (int j = i; j < areaWhereIamCount; ++j) { - areasWhereIam.write[j] = areasWhereIam[j + 1]; + areasWhereIam[j] = areasWhereIam[j + 1]; } wasTheAreaFound = true; break; @@ -883,7 +889,7 @@ void RigidBodyBullet::on_exit_area(AreaBullet *p_area) { } --areaWhereIamCount; - areasWhereIam.write[areaWhereIamCount] = nullptr; // Even if this is not required, I clear the last element to be safe + areasWhereIam[areaWhereIamCount] = nullptr; // Even if this is not required, I clear the last element to be safe if (PhysicsServer3D::AREA_SPACE_OVERRIDE_DISABLED != p_area->get_spOv_mode()) { scratch_space_override_modificator(); } @@ -891,41 +897,35 @@ void RigidBodyBullet::on_exit_area(AreaBullet *p_area) { } void RigidBodyBullet::reload_space_override_modificator() { - // Make sure that kinematic bodies have their total gravity calculated - if (!is_active() && PhysicsServer3D::BODY_MODE_KINEMATIC != mode) { + if (mode == PhysicsServer3D::BODY_MODE_STATIC) { return; } - Vector3 newGravity(0.0, 0.0, 0.0); + Vector3 newGravity; real_t newLinearDamp = MAX(0.0, linearDamp); real_t newAngularDamp = MAX(0.0, angularDamp); - AreaBullet *currentArea; - // Variable used to calculate new gravity for gravity point areas, it is pointed by currentGravity pointer - Vector3 support_gravity(0, 0, 0); - bool stopped = false; - for (int i = areaWhereIamCount - 1; (0 <= i) && !stopped; --i) { - currentArea = areasWhereIam[i]; + for (int i = 0; i < areaWhereIamCount && !stopped; i += 1) { + AreaBullet *currentArea = areasWhereIam[i]; if (!currentArea || PhysicsServer3D::AREA_SPACE_OVERRIDE_DISABLED == currentArea->get_spOv_mode()) { continue; } + Vector3 support_gravity; + /// Here is calculated the gravity if (currentArea->is_spOv_gravityPoint()) { /// It calculates the direction of new gravity support_gravity = currentArea->get_transform().xform(currentArea->get_spOv_gravityVec()) - get_transform().get_origin(); - real_t distanceMag = support_gravity.length(); + + const real_t distanceMag = support_gravity.length(); // Normalized in this way to avoid the double call of function "length()" if (distanceMag == 0) { - support_gravity.x = 0; - support_gravity.y = 0; - support_gravity.z = 0; + support_gravity = Vector3(); } else { - support_gravity.x /= distanceMag; - support_gravity.y /= distanceMag; - support_gravity.z /= distanceMag; + support_gravity /= distanceMag; } /// Here is calculated the final gravity @@ -987,10 +987,17 @@ void RigidBodyBullet::reload_space_override_modificator() { newAngularDamp += space->get_angular_damp(); } - btVector3 newBtGravity; - G_TO_B(newGravity * gravity_scale, newBtGravity); + total_gravity = newGravity; + + if (omit_forces_integration) { + // Custom behaviour. + btBody->setGravity(btVector3(0, 0, 0)); + } else { + btVector3 newBtGravity; + G_TO_B(newGravity * gravity_scale, newBtGravity); + btBody->setGravity(newBtGravity); + } - btBody->setGravity(newBtGravity); btBody->setDamping(newLinearDamp, newAngularDamp); } diff --git a/modules/bullet/rigid_body_bullet.h b/modules/bullet/rigid_body_bullet.h index eb62d0d39e..047645677b 100644 --- a/modules/bullet/rigid_body_bullet.h +++ b/modules/bullet/rigid_body_bullet.h @@ -171,7 +171,7 @@ public: struct KinematicUtilities { RigidBodyBullet *owner; btScalar safe_margin; - Vector<KinematicShape> shapes; + LocalVector<KinematicShape> shapes; KinematicUtilities(RigidBodyBullet *p_owner); ~KinematicUtilities(); @@ -193,6 +193,7 @@ private: PhysicsServer3D::BodyMode mode; GodotMotionState *godotMotionState; btRigidBody *btBody; + Vector3 total_gravity; uint16_t locked_axis = 0; real_t mass = 1; real_t gravity_scale = 1; @@ -202,18 +203,18 @@ private: bool omit_forces_integration = false; bool can_integrate_forces = false; - Vector<CollisionData> collisions; - Vector<RigidBodyBullet *> collision_traces_1; - Vector<RigidBodyBullet *> collision_traces_2; - Vector<RigidBodyBullet *> *prev_collision_traces; - Vector<RigidBodyBullet *> *curr_collision_traces; + LocalVector<CollisionData> collisions; + LocalVector<RigidBodyBullet *> collision_traces_1; + LocalVector<RigidBodyBullet *> collision_traces_2; + LocalVector<RigidBodyBullet *> *prev_collision_traces; + LocalVector<RigidBodyBullet *> *curr_collision_traces; // these parameters are used to avoid vector resize - int maxCollisionsDetection = 0; - int collisionsCount = 0; - int prev_collision_count = 0; + uint32_t maxCollisionsDetection = 0; + uint32_t collisionsCount = 0; + uint32_t prev_collision_count = 0; - Vector<AreaBullet *> areasWhereIam; + LocalVector<AreaBullet *> areasWhereIam; // these parameters are used to avoid vector resize int maxAreasWhereIam = 10; int areaWhereIamCount = 0; @@ -235,21 +236,20 @@ public: _FORCE_INLINE_ btRigidBody *get_bt_rigid_body() { return btBody; } - virtual void main_shape_changed(); - virtual void do_reload_body(); - virtual void set_space(SpaceBullet *p_space); + virtual void main_shape_changed() override; + virtual void do_reload_body() override; + virtual void set_space(SpaceBullet *p_space) override; - virtual void dispatch_callbacks(); + virtual void dispatch_callbacks() override; + virtual void pre_process() override; void set_force_integration_callback(ObjectID p_id, const StringName &p_method, const Variant &p_udata = Variant()); void scratch_space_override_modificator(); - virtual void on_collision_filters_change(); - virtual void on_collision_checker_start(); - virtual void on_collision_checker_end(); - - void set_max_collisions_detection(int p_maxCollisionsDetection) { - ERR_FAIL_COND(0 > p_maxCollisionsDetection); + virtual void do_reload_collision_filters() override; + virtual void on_collision_checker_start() override; + virtual void on_collision_checker_end() override; + void set_max_collisions_detection(uint32_t p_maxCollisionsDetection) { maxCollisionsDetection = p_maxCollisionsDetection; collisions.resize(p_maxCollisionsDetection); @@ -312,19 +312,19 @@ public: void set_angular_velocity(const Vector3 &p_velocity); Vector3 get_angular_velocity() const; - virtual void set_transform__bullet(const btTransform &p_global_transform); - virtual const btTransform &get_transform__bullet() const; + virtual void set_transform__bullet(const btTransform &p_global_transform) override; + virtual const btTransform &get_transform__bullet() const override; - virtual void do_reload_shapes(); + virtual void do_reload_shapes() override; - virtual void on_enter_area(AreaBullet *p_area); - virtual void on_exit_area(AreaBullet *p_area); + virtual void on_enter_area(AreaBullet *p_area) override; + virtual void on_exit_area(AreaBullet *p_area) override; void reload_space_override_modificator(); /// Kinematic void reload_kinematic_shapes(); - virtual void notify_transform_changed(); + virtual void notify_transform_changed() override; private: void _internal_set_mass(real_t p_mass); diff --git a/modules/bullet/shape_bullet.cpp b/modules/bullet/shape_bullet.cpp index f4550c2024..274493ed17 100644 --- a/modules/bullet/shape_bullet.cpp +++ b/modules/bullet/shape_bullet.cpp @@ -504,6 +504,9 @@ void HeightMapShapeBullet::set_data(const Variant &p_data) { int l_width = d["width"]; int l_depth = d["depth"]; + ERR_FAIL_COND_MSG(l_width < 2, "Map width must be at least 2."); + ERR_FAIL_COND_MSG(l_depth < 2, "Map depth must be at least 2."); + // TODO This code will need adjustments if real_t is set to `double`, // because that precision is unnecessary for a heightmap and Bullet doesn't support it... diff --git a/modules/bullet/soft_body_bullet.cpp b/modules/bullet/soft_body_bullet.cpp index 3fccd3d8a2..ee48b3c5f0 100644 --- a/modules/bullet/soft_body_bullet.cpp +++ b/modules/bullet/soft_body_bullet.cpp @@ -346,14 +346,14 @@ void SoftBodyBullet::set_trimesh_body_shape(Vector<int> p_indices, Vector<Vector indices_table.push_back(Vector<int>()); } - indices_table.write[vertex_id].push_back(vs_vertex_index); + indices_table[vertex_id].push_back(vs_vertex_index); vs_indices_to_physics_table.push_back(vertex_id); } } const int indices_map_size(indices_table.size()); - Vector<btScalar> bt_vertices; + LocalVector<btScalar> bt_vertices; { // Parse vertices to bullet @@ -361,13 +361,13 @@ void SoftBodyBullet::set_trimesh_body_shape(Vector<int> p_indices, Vector<Vector const Vector3 *p_vertices_read = p_vertices.ptr(); for (int i = 0; i < indices_map_size; ++i) { - bt_vertices.write[3 * i + 0] = p_vertices_read[indices_table[i][0]].x; - bt_vertices.write[3 * i + 1] = p_vertices_read[indices_table[i][0]].y; - bt_vertices.write[3 * i + 2] = p_vertices_read[indices_table[i][0]].z; + bt_vertices[3 * i + 0] = p_vertices_read[indices_table[i][0]].x; + bt_vertices[3 * i + 1] = p_vertices_read[indices_table[i][0]].y; + bt_vertices[3 * i + 2] = p_vertices_read[indices_table[i][0]].z; } } - Vector<int> bt_triangles; + LocalVector<int> bt_triangles; const int triangles_size(p_indices.size() / 3); { // Parse indices @@ -377,9 +377,9 @@ void SoftBodyBullet::set_trimesh_body_shape(Vector<int> p_indices, Vector<Vector const int *p_indices_read = p_indices.ptr(); for (int i = 0; i < triangles_size; ++i) { - bt_triangles.write[3 * i + 0] = vs_indices_to_physics_table[p_indices_read[3 * i + 2]]; - bt_triangles.write[3 * i + 1] = vs_indices_to_physics_table[p_indices_read[3 * i + 1]]; - bt_triangles.write[3 * i + 2] = vs_indices_to_physics_table[p_indices_read[3 * i + 0]]; + bt_triangles[3 * i + 0] = vs_indices_to_physics_table[p_indices_read[3 * i + 2]]; + bt_triangles[3 * i + 1] = vs_indices_to_physics_table[p_indices_read[3 * i + 1]]; + bt_triangles[3 * i + 2] = vs_indices_to_physics_table[p_indices_read[3 * i + 0]]; } } diff --git a/modules/bullet/soft_body_bullet.h b/modules/bullet/soft_body_bullet.h index ba968f4271..229204b539 100644 --- a/modules/bullet/soft_body_bullet.h +++ b/modules/bullet/soft_body_bullet.h @@ -32,7 +32,6 @@ #define SOFT_BODY_BULLET_H #include "collision_object_bullet.h" -#include "scene/resources/material.h" // TODO remove this please #ifdef None /// This is required to remove the macro None defined by x11 compiler because this word "None" is used internally by Bullet @@ -58,7 +57,7 @@ class SoftBodyBullet : public CollisionObjectBullet { private: btSoftBody *bt_soft_body = nullptr; - Vector<Vector<int>> indices_table; + LocalVector<Vector<int>> indices_table; btSoftBody::Material *mat0; // This is just a copy of pointer managed by btSoftBody bool isScratched = false; @@ -73,7 +72,7 @@ private: real_t pose_matching_coefficient = 0.; // [0,1] real_t damping_coefficient = 0.01; // [0,1] real_t drag_coefficient = 0.; // [0,1] - Vector<int> pinned_nodes; + LocalVector<int> pinned_nodes; // Other property to add //btScalar kVC; // Volume conversation coefficient [0,+inf] @@ -87,15 +86,14 @@ public: SoftBodyBullet(); ~SoftBodyBullet(); - virtual void do_reload_body(); - virtual void set_space(SpaceBullet *p_space); + virtual void do_reload_body() override; + virtual void set_space(SpaceBullet *p_space) override; - virtual void dispatch_callbacks() {} - virtual void on_collision_filters_change() {} - virtual void on_collision_checker_start() {} - virtual void on_collision_checker_end() {} - virtual void on_enter_area(AreaBullet *p_area); - virtual void on_exit_area(AreaBullet *p_area); + virtual void do_reload_collision_filters() override {} + virtual void on_collision_checker_start() override {} + virtual void on_collision_checker_end() override {} + virtual void on_enter_area(AreaBullet *p_area) override; + virtual void on_exit_area(AreaBullet *p_area) override; _FORCE_INLINE_ btSoftBody *get_bt_soft_body() const { return bt_soft_body; } diff --git a/modules/bullet/space_bullet.cpp b/modules/bullet/space_bullet.cpp index 70e137b16d..d0515e7c97 100644 --- a/modules/bullet/space_bullet.cpp +++ b/modules/bullet/space_bullet.cpp @@ -158,10 +158,6 @@ bool BulletPhysicsDirectSpaceState::cast_motion(const RID &p_shape, const Transf btVector3 bt_motion; G_TO_B(p_motion, bt_motion); - if (bt_motion.fuzzyZero()) { - return false; - } - ShapeBullet *shape = space->get_physics_server()->get_shape_owner()->getornull(p_shape); ERR_FAIL_COND_V(!shape, false); @@ -180,6 +176,11 @@ bool BulletPhysicsDirectSpaceState::cast_motion(const RID &p_shape, const Transf btTransform bt_xform_to(bt_xform_from); bt_xform_to.getOrigin() += bt_motion; + if ((bt_xform_to.getOrigin() - bt_xform_from.getOrigin()).fuzzyZero()) { + shape->destroy_bt_shape(btShape); + return false; + } + GodotClosestConvexResultCallback btResult(bt_xform_from.getOrigin(), bt_xform_to.getOrigin(), &p_exclude, p_collide_with_bodies, p_collide_with_areas); btResult.m_collisionFilterGroup = 0; btResult.m_collisionFilterMask = p_collision_mask; @@ -348,16 +349,46 @@ SpaceBullet::~SpaceBullet() { destroy_world(); } +void SpaceBullet::add_to_pre_flush_queue(CollisionObjectBullet *p_co) { + if (p_co->is_in_flush_queue == false) { + p_co->is_in_flush_queue = true; + queue_pre_flush.push_back(p_co); + } +} + +void SpaceBullet::add_to_flush_queue(CollisionObjectBullet *p_co) { + if (p_co->is_in_flush_queue == false) { + p_co->is_in_flush_queue = true; + queue_flush.push_back(p_co); + } +} + +void SpaceBullet::remove_from_any_queue(CollisionObjectBullet *p_co) { + if (p_co->is_in_flush_queue) { + p_co->is_in_flush_queue = false; + queue_pre_flush.erase(p_co); + queue_flush.erase(p_co); + } +} + void SpaceBullet::flush_queries() { - const int size = collision_objects.size(); - CollisionObjectBullet **objects = collision_objects.ptrw(); - for (int i = 0; i < size; i += 1) { - objects[i]->prepare_object_for_dispatch(); - objects[i]->dispatch_callbacks(); + for (uint32_t i = 0; i < queue_pre_flush.size(); i += 1) { + queue_pre_flush[i]->dispatch_callbacks(); + queue_pre_flush[i]->is_in_flush_queue = false; + } + for (uint32_t i = 0; i < queue_flush.size(); i += 1) { + queue_flush[i]->dispatch_callbacks(); + queue_flush[i]->is_in_flush_queue = false; } + queue_pre_flush.clear(); + queue_flush.clear(); } void SpaceBullet::step(real_t p_delta_time) { + for (uint32_t i = 0; i < collision_objects.size(); i += 1) { + collision_objects[i]->pre_process(); + } + delta_time = p_delta_time; dynamicsWorld->stepSimulation(p_delta_time, 0, 0); } @@ -488,6 +519,7 @@ void SpaceBullet::register_collision_object(CollisionObjectBullet *p_object) { } void SpaceBullet::unregister_collision_object(CollisionObjectBullet *p_object) { + remove_from_any_queue(p_object); collision_objects.erase(p_object); } @@ -702,7 +734,7 @@ void SpaceBullet::check_ghost_overlaps() { /// 1. Reset all states for (i = area->overlappingObjects.size() - 1; 0 <= i; --i) { - AreaBullet::OverlappingObjectData &otherObj = area->overlappingObjects.write[i]; + AreaBullet::OverlappingObjectData &otherObj = area->overlappingObjects[i]; // This check prevent the overwrite of ENTER state // if this function is called more times before dispatchCallbacks if (otherObj.state != AreaBullet::OVERLAP_STATE_ENTER) { @@ -982,7 +1014,7 @@ bool SpaceBullet::test_body_motion(RigidBodyBullet *p_body, const Transform &p_f motionVec->end(); #endif - for (int shIndex = 0; shIndex < shape_count && !motion.fuzzyZero(); ++shIndex) { + for (int shIndex = 0; shIndex < shape_count; ++shIndex) { if (p_body->is_shape_disabled(shIndex)) { continue; } @@ -1004,6 +1036,11 @@ bool SpaceBullet::test_body_motion(RigidBodyBullet *p_body, const Transform &p_f btTransform shape_world_to(shape_world_from); shape_world_to.getOrigin() += motion; + if ((shape_world_to.getOrigin() - shape_world_from.getOrigin()).fuzzyZero()) { + motion = btVector3(0, 0, 0); + break; + } + GodotKinClosestConvexResultCallback btResult(shape_world_from.getOrigin(), shape_world_to.getOrigin(), p_body, p_infinite_inertia); btResult.m_collisionFilterGroup = p_body->get_collision_layer(); btResult.m_collisionFilterMask = p_body->get_collision_mask(); diff --git a/modules/bullet/space_bullet.h b/modules/bullet/space_bullet.h index aa9a70594e..897f902fe1 100644 --- a/modules/bullet/space_bullet.h +++ b/modules/bullet/space_bullet.h @@ -31,8 +31,8 @@ #ifndef SPACE_BULLET_H #define SPACE_BULLET_H +#include "core/local_vector.h" #include "core/variant.h" -#include "core/vector.h" #include "godot_result_callbacks.h" #include "rid_bullet.h" #include "servers/physics_server_3d.h" @@ -110,17 +110,23 @@ class SpaceBullet : public RIDBullet { real_t linear_damp = 0.0; real_t angular_damp = 0.0; - Vector<CollisionObjectBullet *> collision_objects; - Vector<AreaBullet *> areas; + LocalVector<CollisionObjectBullet *> queue_pre_flush; + LocalVector<CollisionObjectBullet *> queue_flush; + LocalVector<CollisionObjectBullet *> collision_objects; + LocalVector<AreaBullet *> areas; - Vector<Vector3> contactDebug; - int contactDebugCount = 0; + LocalVector<Vector3> contactDebug; + uint32_t contactDebugCount = 0; real_t delta_time = 0.; public: SpaceBullet(); virtual ~SpaceBullet(); + void add_to_flush_queue(CollisionObjectBullet *p_co); + void add_to_pre_flush_queue(CollisionObjectBullet *p_co); + void remove_from_any_queue(CollisionObjectBullet *p_co); + void flush_queries(); real_t get_delta_time() { return delta_time; } void step(real_t p_delta_time); @@ -177,7 +183,7 @@ public: } _FORCE_INLINE_ void add_debug_contact(const Vector3 &p_contact) { if (contactDebugCount < contactDebug.size()) { - contactDebug.write[contactDebugCount++] = p_contact; + contactDebug[contactDebugCount++] = p_contact; } } _FORCE_INLINE_ Vector<Vector3> get_debug_contacts() { return contactDebug; } diff --git a/modules/camera/camera_ios.mm b/modules/camera/camera_ios.mm index f01135f251..c10b13b2af 100644 --- a/modules/camera/camera_ios.mm +++ b/modules/camera/camera_ios.mm @@ -158,25 +158,31 @@ } else if (dataCbCr == NULL) { print_line("Couldn't access CbCr pixel buffer data"); } else { - UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + UIInterfaceOrientation orientation = UIInterfaceOrientationUnknown; + + if (@available(iOS 13, *)) { + orientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + orientation = [[UIApplication sharedApplication] statusBarOrientation]; +#endif + } + Ref<Image> img[2]; { // do Y - int new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); - int new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); - int _bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + size_t new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + size_t new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); if ((width[0] != new_width) || (height[0] != new_height)) { - // printf("Camera Y plane %i, %i - %i\n", new_width, new_height, bytes_per_row); - width[0] = new_width; height[0] = new_height; img_data[0].resize(new_width * new_height); } uint8_t *w = img_data[0].ptrw(); - memcpy(w.ptr(), dataY, new_width * new_height); + memcpy(w, dataY, new_width * new_height); img[0].instance(); img[0]->create(new_width, new_height, 0, Image::FORMAT_R8, img_data[0]); @@ -184,20 +190,17 @@ { // do CbCr - int new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1); - int new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); - int bytes_per_row = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + size_t new_width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1); + size_t new_height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); if ((width[1] != new_width) || (height[1] != new_height)) { - // printf("Camera CbCr plane %i, %i - %i\n", new_width, new_height, bytes_per_row); - width[1] = new_width; height[1] = new_height; img_data[1].resize(2 * new_width * new_height); } uint8_t *w = img_data[1].ptrw(); - memcpy(w.ptr(), dataCbCr, 2 * new_width * new_height); + memcpy(w, dataCbCr, 2 * new_width * new_height); ///TODO GLES2 doesn't support FORMAT_RG8, need to do some form of conversion img[1].instance(); @@ -359,41 +362,59 @@ void CameraIOS::update_feeds() { // this way of doing things is deprecated but still works, // rewrite to using AVCaptureDeviceDiscoverySession - AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:[NSArray arrayWithObjects:AVCaptureDeviceTypeBuiltInTelephotoCamera, AVCaptureDeviceTypeBuiltInDualCamera, AVCaptureDeviceTypeBuiltInTrueDepthCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera, nil] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified]; + NSMutableArray *deviceTypes = [NSMutableArray array]; - // remove devices that are gone.. - for (int i = feeds.size() - 1; i >= 0; i--) { - Ref<CameraFeedIOS> feed(feeds[i]); + if (@available(iOS 10, *)) { + [deviceTypes addObject:AVCaptureDeviceTypeBuiltInWideAngleCamera]; + [deviceTypes addObject:AVCaptureDeviceTypeBuiltInTelephotoCamera]; - if (feed.is_null()) { - // feed not managed by us - } else if (![session.devices containsObject:feed->get_device()]) { - // remove it from our array, this will also destroy it ;) - remove_feed(feed); - }; - }; + if (@available(iOS 10.2, *)) { + [deviceTypes addObject:AVCaptureDeviceTypeBuiltInDualCamera]; + } - // add new devices.. - for (AVCaptureDevice *device in session.devices) { - bool found = false; + if (@available(iOS 11.1, *)) { + [deviceTypes addObject:AVCaptureDeviceTypeBuiltInTrueDepthCamera]; + } + + AVCaptureDeviceDiscoverySession *session = [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:deviceTypes + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; - for (int i = 0; i < feeds.size() && !found; i++) { + // remove devices that are gone.. + for (int i = feeds.size() - 1; i >= 0; i--) { Ref<CameraFeedIOS> feed(feeds[i]); if (feed.is_null()) { // feed not managed by us - } else if (feed->get_device() == device) { - found = true; + } else if (![session.devices containsObject:feed->get_device()]) { + // remove it from our array, this will also destroy it ;) + remove_feed(feed); }; }; - if (!found) { - Ref<CameraFeedIOS> newfeed; - newfeed.instance(); - newfeed->set_device(device); - add_feed(newfeed); + // add new devices.. + for (AVCaptureDevice *device in session.devices) { + bool found = false; + + for (int i = 0; i < feeds.size() && !found; i++) { + Ref<CameraFeedIOS> feed(feeds[i]); + + if (feed.is_null()) { + // feed not managed by us + } else if (feed->get_device() == device) { + found = true; + }; + }; + + if (!found) { + Ref<CameraFeedIOS> newfeed; + newfeed.instance(); + newfeed->set_device(device); + add_feed(newfeed); + }; }; - }; + } }; CameraIOS::CameraIOS() { diff --git a/modules/csg/csg.cpp b/modules/csg/csg.cpp index 6c0a3a4ca3..47982d519a 100644 --- a/modules/csg/csg.cpp +++ b/modules/csg/csg.cpp @@ -154,6 +154,14 @@ inline bool is_point_in_triangle(const Vector3 &p_point, const Vector3 p_vertice return true; } +inline static bool is_triangle_degenerate(const Vector2 p_vertices[3], real_t p_vertex_snap2) { + real_t det = p_vertices[0].x * p_vertices[1].y - p_vertices[0].x * p_vertices[2].y + + p_vertices[0].y * p_vertices[2].x - p_vertices[0].y * p_vertices[1].x + + p_vertices[1].x * p_vertices[2].y - p_vertices[1].y * p_vertices[2].x; + + return det < p_vertex_snap2; +} + inline static bool are_segements_parallel(const Vector2 p_segment1_points[2], const Vector2 p_segment2_points[2], float p_vertex_snap2) { Vector2 segment1 = p_segment1_points[1] - p_segment1_points[0]; Vector2 segment2 = p_segment2_points[1] - p_segment2_points[0]; @@ -583,8 +591,8 @@ bool CSGBrushOperation::MeshMerge::_bvh_inside(FaceBVH *facebvhptr, int p_max_de // Check if faces are co-planar. if ((current_normal - face_normal).length_squared() < CMP_EPSILON2 && is_point_in_triangle(face_center, current_points)) { - // Only add an intersection if checking a B face. - if (face.from_b) { + // Only add an intersection if not a B face. + if (!face.from_b) { _add_distance(intersectionsA, intersectionsB, current_face.from_b, 0); } } else if (ray_intersects_triangle(face_center, face_normal, current_points, CMP_EPSILON, intersection_point)) { @@ -1117,6 +1125,11 @@ int CSGBrushOperation::Build2DFaces::_insert_point(const Vector2 &p_point) { face_vertices[2].uv }; + // Skip degenerate triangles. + if (is_triangle_degenerate(points, vertex_snap2)) { + continue; + } + // Check if point is existing face vertex. for (int i = 0; i < 3; ++i) { if ((p_point - face_vertices[i].point).length_squared() < vertex_snap2) { @@ -1198,11 +1211,8 @@ int CSGBrushOperation::Build2DFaces::_insert_point(const Vector2 &p_point) { // The new vertex is the last vertex. for (int i = 0; i < 3; ++i) { // Don't create degenerate triangles. - Vector2 edge[2] = { points[i], points[(i + 1) % 3] }; - Vector2 new_edge1[2] = { vertices[new_vertex_idx].point, points[i] }; - Vector2 new_edge2[2] = { vertices[new_vertex_idx].point, points[(i + 1) % 3] }; - if (are_segements_parallel(edge, new_edge1, vertex_snap2) && - are_segements_parallel(edge, new_edge2, vertex_snap2)) { + Vector2 new_points[3] = { points[i], points[(i + 1) % 3], vertices[new_vertex_idx].point }; + if (is_triangle_degenerate(new_points, vertex_snap2)) { continue; } diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index cea006364f..8f2ebc7232 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -132,18 +132,13 @@ void CSGShape3D::_make_dirty() { return; } - if (dirty) { - return; - } - - dirty = true; - if (parent) { parent->_make_dirty(); - } else { - //only parent will do + } else if (!dirty) { call_deferred("_update_shape"); } + + dirty = true; } CSGBrush *CSGShape3D::_get_brush() { @@ -511,6 +506,12 @@ void CSGShape3D::_notification(int p_what) { _make_dirty(); } + if (p_what == NOTIFICATION_TRANSFORM_CHANGED) { + if (use_collision && is_root_shape() && root_collision_instance.is_valid()) { + PhysicsServer3D::get_singleton()->body_set_state(root_collision_instance, PhysicsServer3D::BODY_STATE_TRANSFORM, get_global_transform()); + } + } + if (p_what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED) { if (parent) { parent->_make_dirty(); @@ -646,7 +647,7 @@ CSGShape3D::~CSGShape3D() { ////////////////////////////////// CSGBrush *CSGCombiner3D::_build_brush() { - return nullptr; //does not build anything + return memnew(CSGBrush); //does not build anything } CSGCombiner3D::CSGCombiner3D() { diff --git a/modules/denoise/SCsub b/modules/denoise/SCsub index 0fa65c6296..bf3bd7d073 100644 --- a/modules/denoise/SCsub +++ b/modules/denoise/SCsub @@ -1,7 +1,6 @@ #!/usr/bin/env python import resource_to_cpp -from platform_methods import run_in_subprocess Import("env") Import("env_modules") diff --git a/modules/enet/doc_classes/NetworkedMultiplayerENet.xml b/modules/enet/doc_classes/NetworkedMultiplayerENet.xml index 860da32a22..c908af7479 100644 --- a/modules/enet/doc_classes/NetworkedMultiplayerENet.xml +++ b/modules/enet/doc_classes/NetworkedMultiplayerENet.xml @@ -128,7 +128,7 @@ Enforce ordered packets when using [constant NetworkedMultiplayerPeer.TRANSFER_MODE_UNRELIABLE] (thus behaving similarly to [constant NetworkedMultiplayerPeer.TRANSFER_MODE_UNRELIABLE_ORDERED]). This is the only way to use ordering with the RPC system. </member> <member name="channel_count" type="int" setter="set_channel_count" getter="get_channel_count" default="3"> - The number of channels to be used by ENet. Channels are used to separate different kinds of data. In reliable or ordered mode, for example, the packet delivery order is ensured on a per channel basis. + The number of channels to be used by ENet. Channels are used to separate different kinds of data. In reliable or ordered mode, for example, the packet delivery order is ensured on a per-channel basis. This is done to combat latency and reduces ordering restrictions on packets. The delivery status of a packet in one channel won't stall the delivery of other packets in another channel. </member> <member name="compression_mode" type="int" setter="set_compression_mode" getter="get_compression_mode" enum="NetworkedMultiplayerENet.CompressionMode" default="0"> The compression method used for network packets. These have different tradeoffs of compression speed versus bandwidth, you may need to test which one works best for your use case if you use compression at all. @@ -141,7 +141,7 @@ Enable or disable the server feature that notifies clients of other peers' connection/disconnection, and relays messages between them. When this option is [code]false[/code], clients won't be automatically notified of other peers and won't be able to send them packets through the server. </member> <member name="transfer_channel" type="int" setter="set_transfer_channel" getter="get_transfer_channel" default="-1"> - Set the default channel to be used to transfer data. By default, this value is [code]-1[/code] which means that ENet will only use 2 channels, one for reliable and one for unreliable packets. Channel [code]0[/code] is reserved, and cannot be used. Setting this member to any value between [code]0[/code] and [member channel_count] (excluded) will force ENet to use that channel for sending data. + Set the default channel to be used to transfer data. By default, this value is [code]-1[/code] which means that ENet will only use 2 channels: one for reliable packets, and one for unreliable packets. The channel [code]0[/code] is reserved and cannot be used. Setting this member to any value between [code]0[/code] and [member channel_count] (excluded) will force ENet to use that channel for sending data. See [member channel_count] for more information about ENet channels. </member> <member name="transfer_mode" type="int" setter="set_transfer_mode" getter="get_transfer_mode" override="true" enum="NetworkedMultiplayerPeer.TransferMode" default="2" /> <member name="use_dtls" type="bool" setter="set_dtls_enabled" getter="is_dtls_enabled" default="false"> diff --git a/modules/enet/networked_multiplayer_enet.cpp b/modules/enet/networked_multiplayer_enet.cpp index 4e7698b67c..64977ad237 100644 --- a/modules/enet/networked_multiplayer_enet.cpp +++ b/modules/enet/networked_multiplayer_enet.cpp @@ -104,6 +104,7 @@ Error NetworkedMultiplayerENet::create_server(int p_port, int p_max_clients, int if (dtls_enabled) { enet_host_dtls_server_setup(host, dtls_key.ptr(), dtls_cert.ptr()); } + enet_host_refuse_new_connections(host, refuse_connections); #endif _setup_compressor(); @@ -160,6 +161,7 @@ Error NetworkedMultiplayerENet::create_client(const String &p_address, int p_por if (dtls_enabled) { enet_host_dtls_client_setup(host, dtls_cert.ptr(), dtls_verify, p_address.utf8().get_data()); } + enet_host_refuse_new_connections(host, refuse_connections); #endif _setup_compressor(); @@ -640,6 +642,11 @@ int NetworkedMultiplayerENet::get_unique_id() const { void NetworkedMultiplayerENet::set_refuse_new_connections(bool p_enable) { refuse_connections = p_enable; +#ifdef GODOT_ENET + if (active) { + enet_host_refuse_new_connections(host, p_enable); + } +#endif } bool NetworkedMultiplayerENet::is_refusing_new_connections() const { diff --git a/modules/etc/image_etc.cpp b/modules/etc/image_etc.cpp index 9b6d8a2d35..d1ba3dc355 100644 --- a/modules/etc/image_etc.cpp +++ b/modules/etc/image_etc.cpp @@ -106,7 +106,7 @@ static void _compress_etc(Image *p_img, float p_lossy_quality, bool force_etc1_f // If VRAM compression is using ETC, but image has alpha, convert to RGBA4444 or LA8 // This saves space while maintaining the alpha channel if (detected_channels == Image::USED_CHANNELS_RGBA) { - + if (p_img->has_mipmaps()) { // Image doesn't support mipmaps with RGBA4444 textures p_img->clear_mipmaps(); diff --git a/modules/gdnative/SCsub b/modules/gdnative/SCsub index cab05549d2..0e2291c1f9 100644 --- a/modules/gdnative/SCsub +++ b/modules/gdnative/SCsub @@ -22,13 +22,12 @@ SConscript("pluginscript/SCsub") SConscript("videodecoder/SCsub") -from platform_methods import run_in_subprocess import gdnative_builders _, gensource = env_gdnative.CommandNoCache( ["include/gdnative_api_struct.gen.h", "gdnative_api_struct.gen.cpp"], "gdnative_api.json", - run_in_subprocess(gdnative_builders.build_gdnative_api_struct), + env.Run(gdnative_builders.build_gdnative_api_struct, "Generating GDNative API."), ) env_gdnative.add_source_files(env.modules_sources, [gensource]) diff --git a/modules/gdnative/gdnative/aabb.cpp b/modules/gdnative/gdnative/aabb.cpp index 7f22c7dfe3..d5970e8004 100644 --- a/modules/gdnative/gdnative/aabb.cpp +++ b/modules/gdnative/gdnative/aabb.cpp @@ -81,6 +81,13 @@ godot_string GDAPI godot_aabb_as_string(const godot_aabb *p_self) { return ret; } +godot_aabb GDAPI godot_aabb_abs(const godot_aabb *p_self) { + godot_aabb dest; + const AABB *self = (const AABB *)p_self; + *((AABB *)&dest) = self->abs(); + return dest; +} + godot_real GDAPI godot_aabb_get_area(const godot_aabb *p_self) { const AABB *self = (const AABB *)p_self; return self->get_area(); diff --git a/modules/gdnative/gdnative/plane.cpp b/modules/gdnative/gdnative/plane.cpp index 663937f906..d4ed8d00f4 100644 --- a/modules/gdnative/gdnative/plane.cpp +++ b/modules/gdnative/gdnative/plane.cpp @@ -79,13 +79,6 @@ godot_vector3 GDAPI godot_plane_center(const godot_plane *p_self) { return dest; } -godot_vector3 GDAPI godot_plane_get_any_point(const godot_plane *p_self) { - godot_vector3 dest; - const Plane *self = (const Plane *)p_self; - *((Vector3 *)&dest) = self->get_any_point(); - return dest; -} - godot_bool GDAPI godot_plane_is_point_over(const godot_plane *p_self, const godot_vector3 *p_point) { const Plane *self = (const Plane *)p_self; const Vector3 *point = (const Vector3 *)p_point; diff --git a/modules/gdnative/gdnative/string.cpp b/modules/gdnative/gdnative/string.cpp index 8b0c7474e8..26c40b625c 100644 --- a/modules/gdnative/gdnative/string.cpp +++ b/modules/gdnative/gdnative/string.cpp @@ -541,13 +541,7 @@ godot_string GDAPI godot_string_substr(const godot_string *p_self, godot_int p_f return result; } -double GDAPI godot_string_to_double(const godot_string *p_self) { - const String *self = (const String *)p_self; - - return self->to_double(); -} - -godot_real GDAPI godot_string_to_float(const godot_string *p_self) { +double GDAPI godot_string_to_float(const godot_string *p_self) { const String *self = (const String *)p_self; return self->to_float(); @@ -583,8 +577,8 @@ godot_string GDAPI godot_string_camelcase_to_underscore_lowercased(const godot_s return result; } -double GDAPI godot_string_char_to_double(const char *p_what) { - return String::to_double(p_what); +double GDAPI godot_string_char_to_float(const char *p_what) { + return String::to_float(p_what); } godot_int GDAPI godot_string_char_to_int(const char *p_what) { @@ -621,8 +615,8 @@ int64_t GDAPI godot_string_to_int64(const godot_string *p_self) { return self->to_int(); } -double GDAPI godot_string_unicode_char_to_double(const wchar_t *p_str, const wchar_t **r_end) { - return String::to_double(p_str, r_end); +double GDAPI godot_string_unicode_char_to_float(const wchar_t *p_str, const wchar_t **r_end) { + return String::to_float(p_str, r_end); } godot_string GDAPI godot_string_get_slice(const godot_string *p_self, godot_string p_splitter, godot_int p_slice) { diff --git a/modules/gdnative/gdnative_api.json b/modules/gdnative/gdnative_api.json index eb122089b6..8ccf44ff1a 100644 --- a/modules/gdnative/gdnative_api.json +++ b/modules/gdnative/gdnative_api.json @@ -54,6 +54,13 @@ ] }, { + "name": "godot_aabb_abs", + "return_type": "godot_aabb", + "arguments": [ + ["const godot_aabb *", "p_self"] + ] + }, + { "name": "godot_aabb_get_area", "return_type": "godot_real", "arguments": [ @@ -2981,13 +2988,6 @@ ] }, { - "name": "godot_plane_get_any_point", - "return_type": "godot_vector3", - "arguments": [ - ["const godot_plane *", "p_self"] - ] - }, - { "name": "godot_plane_is_point_over", "return_type": "godot_bool", "arguments": [ @@ -4237,15 +4237,8 @@ ] }, { - "name": "godot_string_to_double", - "return_type": "double", - "arguments": [ - ["const godot_string *", "p_self"] - ] - }, - { "name": "godot_string_to_float", - "return_type": "godot_real", + "return_type": "double", "arguments": [ ["const godot_string *", "p_self"] ] @@ -4279,7 +4272,7 @@ ] }, { - "name": "godot_string_char_to_double", + "name": "godot_string_char_to_float", "return_type": "double", "arguments": [ ["const char *", "p_what"] @@ -4337,7 +4330,7 @@ ] }, { - "name": "godot_string_unicode_char_to_double", + "name": "godot_string_unicode_char_to_float", "return_type": "double", "arguments": [ ["const wchar_t *", "p_str"], diff --git a/modules/gdnative/gdnative_builders.py b/modules/gdnative/gdnative_builders.py index a6f8afb85b..28e4957b2f 100644 --- a/modules/gdnative/gdnative_builders.py +++ b/modules/gdnative/gdnative_builders.py @@ -74,7 +74,7 @@ def _build_gdnative_api_struct_header(api): ret_val += [ "typedef struct godot_gdnative_core_" - + ("{0}_{1}".format(core["version"]["major"], core["version"]["minor"])) + + "{0}_{1}".format(core["version"]["major"], core["version"]["minor"]) + "_api_struct {", "\tunsigned int type;", "\tgodot_gdnative_api_version version;", @@ -185,7 +185,7 @@ def _build_gdnative_api_struct_source(api): ret_val += [ "extern const godot_gdnative_core_" - + ("{0}_{1}_api_struct api_{0}_{1}".format(core["version"]["major"], core["version"]["minor"])) + + "{0}_{1}_api_struct api_{0}_{1}".format(core["version"]["major"], core["version"]["minor"]) + " = {", "\tGDNATIVE_" + core["type"] + ",", "\t{" + str(core["version"]["major"]) + ", " + str(core["version"]["minor"]) + "},", diff --git a/modules/gdnative/gdnative_library_editor_plugin.cpp b/modules/gdnative/gdnative_library_editor_plugin.cpp index 1d4d188f23..fdd755845f 100644 --- a/modules/gdnative/gdnative_library_editor_plugin.cpp +++ b/modules/gdnative/gdnative_library_editor_plugin.cpp @@ -146,7 +146,7 @@ void GDNativeLibraryEditor::_on_item_button(Object *item, int column, int id) { } file_dialog->set_file_mode(mode); - file_dialog->popup_centered_ratio(); + file_dialog->popup_file_dialog(); } else if (id == BUTTON_CLEAR_LIBRARY) { _set_target_value(section, target, ""); diff --git a/modules/gdnative/include/gdnative/aabb.h b/modules/gdnative/include/gdnative/aabb.h index 9f41e9d4c6..c776297944 100644 --- a/modules/gdnative/include/gdnative/aabb.h +++ b/modules/gdnative/include/gdnative/aabb.h @@ -69,6 +69,8 @@ void GDAPI godot_aabb_set_size(const godot_aabb *p_self, const godot_vector3 *p_ godot_string GDAPI godot_aabb_as_string(const godot_aabb *p_self); +godot_aabb GDAPI godot_aabb_abs(const godot_aabb *p_self); + godot_real GDAPI godot_aabb_get_area(const godot_aabb *p_self); godot_bool GDAPI godot_aabb_has_no_area(const godot_aabb *p_self); diff --git a/modules/gdnative/include/gdnative/plane.h b/modules/gdnative/include/gdnative/plane.h index b759a8cc1a..9843056489 100644 --- a/modules/gdnative/include/gdnative/plane.h +++ b/modules/gdnative/include/gdnative/plane.h @@ -68,8 +68,6 @@ godot_plane GDAPI godot_plane_normalized(const godot_plane *p_self); godot_vector3 GDAPI godot_plane_center(const godot_plane *p_self); -godot_vector3 GDAPI godot_plane_get_any_point(const godot_plane *p_self); - godot_bool GDAPI godot_plane_is_point_over(const godot_plane *p_self, const godot_vector3 *p_point); godot_real GDAPI godot_plane_distance_to(const godot_plane *p_self, const godot_vector3 *p_point); diff --git a/modules/gdnative/include/gdnative/string.h b/modules/gdnative/include/gdnative/string.h index dfd4fcab89..d89383dc1b 100644 --- a/modules/gdnative/include/gdnative/string.h +++ b/modules/gdnative/include/gdnative/string.h @@ -145,14 +145,13 @@ godot_string GDAPI godot_string_rpad_with_custom_character(const godot_string *p godot_real GDAPI godot_string_similarity(const godot_string *p_self, const godot_string *p_string); godot_string GDAPI godot_string_sprintf(const godot_string *p_self, const godot_array *p_values, godot_bool *p_error); godot_string GDAPI godot_string_substr(const godot_string *p_self, godot_int p_from, godot_int p_chars); -double GDAPI godot_string_to_double(const godot_string *p_self); -godot_real GDAPI godot_string_to_float(const godot_string *p_self); +double GDAPI godot_string_to_float(const godot_string *p_self); godot_int GDAPI godot_string_to_int(const godot_string *p_self); godot_string GDAPI godot_string_camelcase_to_underscore(const godot_string *p_self); godot_string GDAPI godot_string_camelcase_to_underscore_lowercased(const godot_string *p_self); godot_string GDAPI godot_string_capitalize(const godot_string *p_self); -double GDAPI godot_string_char_to_double(const char *p_what); +double GDAPI godot_string_char_to_float(const char *p_what); godot_int GDAPI godot_string_char_to_int(const char *p_what); int64_t GDAPI godot_string_wchar_to_int(const wchar_t *p_str); godot_int GDAPI godot_string_char_to_int_with_len(const char *p_what, godot_int p_len); @@ -160,7 +159,7 @@ int64_t GDAPI godot_string_char_to_int64_with_len(const wchar_t *p_str, int p_le int64_t GDAPI godot_string_hex_to_int64(const godot_string *p_self); int64_t GDAPI godot_string_hex_to_int64_with_prefix(const godot_string *p_self); int64_t GDAPI godot_string_to_int64(const godot_string *p_self); -double GDAPI godot_string_unicode_char_to_double(const wchar_t *p_str, const wchar_t **r_end); +double GDAPI godot_string_unicode_char_to_float(const wchar_t *p_str, const wchar_t **r_end); godot_int GDAPI godot_string_get_slice_count(const godot_string *p_self, godot_string p_splitter); godot_string GDAPI godot_string_get_slice(const godot_string *p_self, godot_string p_splitter, godot_int p_slice); diff --git a/modules/gdnative/nativescript/api_generator.cpp b/modules/gdnative/nativescript/api_generator.cpp index 62f2ec5024..019fa0d1f8 100644 --- a/modules/gdnative/nativescript/api_generator.cpp +++ b/modules/gdnative/nativescript/api_generator.cpp @@ -139,7 +139,7 @@ static String get_type_name(const PropertyInfo &info) { } /* - * Some comparison helper functions we need + * Some comparison helper functions we need */ struct MethodInfoComparator { @@ -214,7 +214,7 @@ List<ClassAPI> generate_c_api_classes() { { List<StringName> inheriters; ClassDB::get_inheriters_from_class("Reference", &inheriters); - bool is_reference = !!inheriters.find(class_name); + bool is_reference = !!inheriters.find(class_name) || class_name == "Reference"; // @Unclear class_api.is_reference = !class_api.is_singleton && is_reference; } @@ -452,6 +452,7 @@ static List<String> generate_c_api_json(const List<ClassAPI> &p_api) { source.push_back("\t\t\t\t\t{\n"); source.push_back("\t\t\t\t\t\t\"name\": \"" + e->get().argument_names[i] + "\",\n"); source.push_back("\t\t\t\t\t\t\"type\": \"" + e->get().argument_types[i] + "\",\n"); + source.push_back(String("\t\t\t\t\t\t\"has_default_value\": ") + (e->get().default_arguments.has(i) ? "true" : "false") + ",\n"); source.push_back("\t\t\t\t\t\t\"default_value\": \"" + (e->get().default_arguments.has(i) ? (String)e->get().default_arguments[i] : "") + "\"\n"); source.push_back(String("\t\t\t\t\t}") + ((i < e->get().argument_names.size() - 1) ? "," : "") + "\n"); } diff --git a/modules/gdnative/nativescript/nativescript.cpp b/modules/gdnative/nativescript/nativescript.cpp index 94aa2125c2..632f4e5fee 100644 --- a/modules/gdnative/nativescript/nativescript.cpp +++ b/modules/gdnative/nativescript/nativescript.cpp @@ -991,7 +991,8 @@ void NativeScriptInstance::notification(int p_notification) { Variant value = p_notification; const Variant *args[1] = { &value }; - call_multilevel("_notification", args, 1); + Callable::CallError error; + call("_notification", args, 1, error); } String NativeScriptInstance::to_string(bool *r_valid) { @@ -1087,31 +1088,6 @@ ScriptLanguage *NativeScriptInstance::get_language() { return NativeScriptLanguage::get_singleton(); } -void NativeScriptInstance::call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount) { - NativeScriptDesc *script_data = GET_SCRIPT_DESC(); - - while (script_data) { - Map<StringName, NativeScriptDesc::Method>::Element *E = script_data->methods.find(p_method); - if (E) { - godot_variant res = E->get().method.method((godot_object *)owner, - E->get().method.method_data, - userdata, - p_argcount, - (godot_variant **)p_args); - godot_variant_destroy(&res); - } - script_data = script_data->base_data; - } -} - -void NativeScriptInstance::call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount) { - NativeScriptDesc *script_data = GET_SCRIPT_DESC(); - - if (script_data) { - _ml_call_reversed(script_data, p_method, p_args, p_argcount); - } -} - NativeScriptInstance::~NativeScriptInstance() { NativeScriptDesc *script_data = GET_SCRIPT_DESC(); diff --git a/modules/gdnative/nativescript/nativescript.h b/modules/gdnative/nativescript/nativescript.h index e709ce2337..145bf7dcb6 100644 --- a/modules/gdnative/nativescript/nativescript.h +++ b/modules/gdnative/nativescript/nativescript.h @@ -231,9 +231,6 @@ public: virtual ScriptLanguage *get_language(); - virtual void call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount); - virtual void call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount); - virtual void refcount_incremented(); virtual bool refcount_decremented(); diff --git a/modules/gdnative/pluginscript/pluginscript_instance.h b/modules/gdnative/pluginscript/pluginscript_instance.h index 6309b6fde3..690d1a0432 100644 --- a/modules/gdnative/pluginscript/pluginscript_instance.h +++ b/modules/gdnative/pluginscript/pluginscript_instance.h @@ -62,12 +62,6 @@ public: virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error); - // Rely on default implementations provided by ScriptInstance for the moment. - // Note that multilevel call could be removed in 3.0 release, so stay tuned - // (see https://godotengine.org/qa/9244/can-override-the-_ready-and-_process-functions-child-classes) - //virtual void call_multilevel(const StringName& p_method,const Variant** p_args,int p_argcount); - //virtual void call_multilevel_reversed(const StringName& p_method,const Variant** p_args,int p_argcount); - virtual void notification(int p_notification); virtual Ref<Script> get_script() const; diff --git a/modules/gdnative/register_types.cpp b/modules/gdnative/register_types.cpp index d1b1513ac3..3a2d0b09a3 100644 --- a/modules/gdnative/register_types.cpp +++ b/modules/gdnative/register_types.cpp @@ -171,7 +171,7 @@ void GDNativeExportPlugin::_export_file(const String &p_path, const String &p_ty String entry_lib_path = config->get_value("entry", key); if (entry_lib_path.begins_with("res://") && entry_lib_path.ends_with(".a")) { // If we find static library that was used for export - // we should add a fake loopup table. + // we should add a fake lookup table. // In case of dynamic library being used, // this symbols will not cause any issues with library loading. should_fake_dynamic = true; diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index f04cb4b4c3..9e40a69712 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -52,7 +52,7 @@ <argument index="0" name="s" type="float"> </argument> <description> - Returns the absolute value of parameter [code]s[/code] (i.e. unsigned value, works for integer and float). + Returns the absolute value of parameter [code]s[/code] (i.e. positive value). [codeblock] # a is 1 a = abs(-1) @@ -112,7 +112,7 @@ </argument> <description> Returns the arc tangent of [code]s[/code] in radians. Use it to get the angle from an angle's tangent in trigonometry: [code]atan(tan(angle)) == angle[/code]. - The method cannot know in which quadrant the angle should fall. See [method atan2] if you always want an exact angle. + The method cannot know in which quadrant the angle should fall. See [method atan2] if you have both [code]y[/code] and [code]x[/code]. [codeblock] a = atan(0.5) # a is 0.463648 [/codeblock] @@ -127,6 +127,7 @@ </argument> <description> Returns the arc tangent of [code]y/x[/code] in radians. Use to get the angle of tangent [code]y/x[/code]. To compute the value, the method takes into account the sign of both arguments in order to determine the quadrant. + Important note: The Y coordinate comes first, by convention. [codeblock] a = atan2(0, -1) # a is 3.141593 [/codeblock] @@ -161,7 +162,7 @@ <argument index="0" name="s" type="float"> </argument> <description> - Rounds [code]s[/code] upward, returning the smallest integral value that is not less than [code]s[/code]. + Rounds [code]s[/code] upward (towards positive infinity), returning the smallest whole number that is not less than [code]s[/code]. [codeblock] i = ceil(1.45) # i is 2 i = ceil(1.001) # i is 2 @@ -283,7 +284,7 @@ <argument index="0" name="deg" type="float"> </argument> <description> - Returns degrees converted to radians. + Converts an angle expressed in degrees to radians. [codeblock] # r is 3.141593 r = deg2rad(180) @@ -307,7 +308,7 @@ <argument index="1" name="curve" type="float"> </argument> <description> - Easing function, based on exponent. 0 is constant, 1 is linear, 0 to 1 is ease-in, 1+ is ease out. Negative values are in-out/out in. + Easing function, based on exponent. The curve values are: 0 is constant, 1 is linear, 0 to 1 is ease-in, 1+ is ease out. Negative values are in-out/out in. </description> </method> <method name="exp"> @@ -317,7 +318,7 @@ </argument> <description> The natural exponential function. It raises the mathematical constant [b]e[/b] to the power of [code]s[/code] and returns it. - [b]e[/b] has an approximate value of 2.71828. + [b]e[/b] has an approximate value of 2.71828, and can be obtained with [code]exp(1)[/code]. For exponents to other bases use the method [method pow]. [codeblock] a = exp(2) # Approximately 7.39 @@ -330,7 +331,7 @@ <argument index="0" name="s" type="float"> </argument> <description> - Rounds [code]s[/code] to the closest smaller integer and returns it. + Rounds [code]s[/code] downward (towards negative infinity), returning the largest whole number that is not more than [code]s[/code]. [codeblock] # a is 2.0 a = floor(2.99) @@ -504,6 +505,8 @@ </argument> <description> Returns [code]true[/code] if [code]a[/code] and [code]b[/code] are approximately equal to each other. + Here, approximately equal means that [code]a[/code] and [code]b[/code] are within a small internal epsilon of each other, which scales with the magnitude of the numbers. + Infinity values of the same sign are considered equal. </description> </method> <method name="is_inf"> @@ -530,7 +533,7 @@ <argument index="0" name="s" type="float"> </argument> <description> - Returns whether [code]s[/code] is a NaN (Not-A-Number) value. + Returns whether [code]s[/code] is a NaN ("Not a Number" or invalid) value. </description> </method> <method name="is_zero_approx"> @@ -540,6 +543,7 @@ </argument> <description> Returns [code]true[/code] if [code]s[/code] is zero or almost zero. + This method is faster than using [method is_equal_approx] with one value as zero. </description> </method> <method name="len"> @@ -639,6 +643,7 @@ [codeblock] log(10) # Returns 2.302585 [/codeblock] + [b]Note:[/b] The logarithm of [code]0[/code] returns [code]-inf[/code], while negative values return [code]-nan[/code]. </description> </method> <method name="max"> @@ -684,7 +689,9 @@ Moves [code]from[/code] toward [code]to[/code] by the [code]delta[/code] value. Use a negative [code]delta[/code] value to move away. [codeblock] + move_toward(5, 10, 4) # Returns 9 move_toward(10, 5, 4) # Returns 6 + move_toward(10, 5, -1.5) # Returns 11.5 [/codeblock] </description> </method> @@ -694,12 +701,17 @@ <argument index="0" name="value" type="int"> </argument> <description> - Returns the nearest larger power of 2 for integer [code]value[/code]. + Returns the nearest equal or larger power of 2 for integer [code]value[/code]. + In other words, returns the smallest value [code]a[/code] where [code]a = pow(2, n)[/code] such that [code]value <= a[/code] for some non-negative integer [code]n[/code]. [codeblock] nearest_po2(3) # Returns 4 nearest_po2(4) # Returns 4 nearest_po2(5) # Returns 8 + + nearest_po2(0) # Returns 0 (this may not be what you expect) + nearest_po2(-1) # Returns 0 (this may not be what you expect) [/codeblock] + [b]WARNING:[/b] Due to the way it is implemented, this function returns [code]0[/code] rather than [code]1[/code] for non-positive values of [code]value[/code] (in reality, 1 is the smallest integer power of 2). </description> </method> <method name="ord"> @@ -723,16 +735,17 @@ <argument index="0" name="json" type="String"> </argument> <description> - Parse JSON text to a Variant (use [method typeof] to check if it is what you expect). - Be aware that the JSON specification does not define integer or float types, but only a number type. Therefore, parsing a JSON text will convert all numerical values to [float] types. - Note that JSON objects do not preserve key order like Godot dictionaries, thus you should not rely on keys being in a certain order if a dictionary is constructed from JSON. In contrast, JSON arrays retain the order of their elements: + Parse JSON text to a Variant. (Use [method typeof] to check if the Variant's type is what you expect.) + [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, parsing a JSON text will convert all numerical values to [float] types. + [b]Note:[/b] JSON objects do not preserve key order like Godot dictionaries, thus, you should not rely on keys being in a certain order if a dictionary is constructed from JSON. In contrast, JSON arrays retain the order of their elements: [codeblock] - p = parse_json('["a", "b", "c"]') - if typeof(p) == TYPE_ARRAY: - print(p[0]) # Prints a + var p = JSON.parse('["hello", "world", "!"]') + if typeof(p.result) == TYPE_ARRAY: + print(p.result[0]) # Prints "hello" else: - print("unexpected results") + push_error("Unexpected results.") [/codeblock] + See also [JSON] for an alternative way to parse JSON text. </description> </method> <method name="polar2cartesian"> @@ -907,7 +920,7 @@ <argument index="0" name="rad" type="float"> </argument> <description> - Converts from radians to degrees. + Converts an angle expressed in radians to degrees. [codeblock] rad2deg(0.523599) # Returns 30 [/codeblock] @@ -1026,7 +1039,7 @@ <argument index="0" name="s" type="float"> </argument> <description> - Returns the integral value that is nearest to [code]s[/code], with halfway cases rounded away from zero. + Rounds [code]s[/code] to the nearest whole number, with halfway cases rounded away from zero. [codeblock] round(2.6) # Returns 3 [/codeblock] @@ -1091,12 +1104,15 @@ </argument> <argument index="1" name="to" type="float"> </argument> - <argument index="2" name="weight" type="float"> + <argument index="2" name="s" type="float"> </argument> <description> - Returns a number smoothly interpolated between the [code]from[/code] and [code]to[/code], based on the [code]weight[/code]. Similar to [method lerp], but interpolates faster at the beginning and slower at the end. + Returns the result of smoothly interpolating the value of [code]s[/code] between [code]0[/code] and [code]1[/code], based on the where [code]s[/code] lies with respect to the edges [code]from[/code] and [code]to[/code]. + The return value is [code]0[/code] if [code]s <= from[/code], and [code]1[/code] if [code]s >= to[/code]. If [code]s[/code] lies between [code]from[/code] and [code]to[/code], the returned value follows an S-shaped curve that maps [code]s[/code] between [code]0[/code] and [code]1[/code]. + This S-shaped curve is the cubic Hermite interpolator, given by [code]f(s) = 3*s^2 - 2*s^3[/code]. [codeblock] - smoothstep(0, 2, 0.5) # Returns 0.15 + smoothstep(0, 2, -5.0) # Returns 0.0 + smoothstep(0, 2, 0.5) # Returns 0.15625 smoothstep(0, 2, 1.0) # Returns 0.5 smoothstep(0, 2, 2.0) # Returns 1.0 [/codeblock] @@ -1108,10 +1124,11 @@ <argument index="0" name="s" type="float"> </argument> <description> - Returns the square root of [code]s[/code]. + Returns the square root of [code]s[/code], where [code]s[/code] is a non-negative number. [codeblock] sqrt(9) # Returns 3 [/codeblock] + [b]Note:[/b]Negative values of [code]s[/code] return NaN. If you need negative inputs, use [code]System.Numerics.Complex[/code] in C#. </description> </method> <method name="step_decimals"> @@ -1204,12 +1221,16 @@ <argument index="0" name="var" type="Variant"> </argument> <description> - Converts a Variant [code]var[/code] to JSON text and return the result. Useful for serializing data to store or send over the network. + Converts a [Variant] [code]var[/code] to JSON text and return the result. Useful for serializing data to store or send over the network. [codeblock] + # Both numbers below are integers. a = { "a": 1, "b": 2 } b = to_json(a) print(b) # {"a":1, "b":2} + # Both numbers above are floats, even if they display without any decimal places. [/codeblock] + [b]Note:[/b] The JSON specification does not define integer or float types, but only a [i]number[/i] type. Therefore, converting a [Variant] to JSON text will convert all numerical values to [float] types. + See also [JSON] for an alternative way to convert a [Variant] to JSON text. </description> </method> <method name="type_exists"> @@ -1252,9 +1273,9 @@ j = to_json([1, 2, 3]) v = validate_json(j) if not v: - print("valid") + print("Valid JSON.") else: - prints("invalid", v) + push_error("Invalid JSON: " + v) [/codeblock] </description> </method> @@ -1312,27 +1333,19 @@ Wraps float [code]value[/code] between [code]min[/code] and [code]max[/code]. Usable for creating loop-alike behavior or infinite surfaces. [codeblock] - # a is 0.5 - a = wrapf(10.5, 0.0, 10.0) - [/codeblock] - [codeblock] - # a is 9.5 - a = wrapf(-0.5, 0.0, 10.0) - [/codeblock] - [codeblock] - # Infinite loop between 0.0 and 0.99 - f = wrapf(f + 0.1, 0.0, 1.0) + # Infinite loop between 5.0 and 9.9 + value = wrapf(value + 0.1, 5.0, 10.0) [/codeblock] [codeblock] # Infinite rotation (in radians) angle = wrapf(angle + 0.1, 0.0, TAU) [/codeblock] - [b]Note:[/b] If you just want to wrap between 0.0 and [code]n[/code] (where [code]n[/code] is a positive floating-point value), it is better for performance to use the [method fmod] method like [code]fmod(number, n)[/code]. - [code]wrapf[/code] is more flexible than using the [method fmod] approach by giving the user a simple control over the minimum value. It also fully supports negative numbers, e.g. [codeblock] # Infinite rotation (in radians) angle = wrapf(angle + 0.1, -PI, PI) [/codeblock] + [b]Note:[/b] If [code]min[/code] is [code]0[/code], this is equivalent to [method fposmod], so prefer using that instead. + [code]wrapf[/code] is more flexible than using the [method fposmod] approach by giving the user control over the minimum value. </description> </method> <method name="wrapi"> @@ -1348,75 +1361,30 @@ Wraps integer [code]value[/code] between [code]min[/code] and [code]max[/code]. Usable for creating loop-alike behavior or infinite surfaces. [codeblock] - # a is 0 - a = wrapi(10, 0, 10) + # Infinite loop between 5 and 9 + frame = wrapi(frame + 1, 5, 10) [/codeblock] [codeblock] - # a is 9 - a = wrapi(-1, 0, 10) - [/codeblock] - [codeblock] - # Infinite loop between 0 and 9 - frame = wrapi(frame + 1, 0, 10) - [/codeblock] - [b]Note:[/b] If you just want to wrap between 0 and [code]n[/code] (where [code]n[/code] is a positive integer value), it is better for performance to use the modulo operator like [code]number % n[/code]. - [code]wrapi[/code] is more flexible than using the modulo approach by giving the user a simple control over the minimum value. It also fully supports negative numbers, e.g. - [codeblock] # result is -2 var result = wrapi(-6, -5, -1) [/codeblock] - </description> - </method> - <method name="yield"> - <return type="GDScriptFunctionState"> - </return> - <argument index="0" name="object" type="Object" default="null"> - </argument> - <argument index="1" name="signal" type="String" default=""""> - </argument> - <description> - Stops the function execution and returns the current suspended state to the calling function. - From the caller, call [method GDScriptFunctionState.resume] on the state to resume execution. This invalidates the state. Within the resumed function, [code]yield()[/code] returns whatever was passed to the [code]resume()[/code] function call. - If passed an object and a signal, the execution is resumed when the object emits the given signal. In this case, [code]yield()[/code] returns the argument passed to [code]emit_signal()[/code] if the signal takes only one argument, or an array containing all the arguments passed to [code]emit_signal()[/code] if the signal takes multiple arguments. - You can also use [code]yield[/code] to wait for a function to finish: - [codeblock] - func _ready(): - yield(countdown(), "completed") # waiting for the countdown() function to complete - print('Ready') - - func countdown(): - yield(get_tree(), "idle_frame") # returns a GDScriptFunctionState object to _ready() - print(3) - yield(get_tree().create_timer(1.0), "timeout") - print(2) - yield(get_tree().create_timer(1.0), "timeout") - print(1) - yield(get_tree().create_timer(1.0), "timeout") - - # prints: - # 3 - # 2 - # 1 - # Ready - [/codeblock] - When yielding on a function, the [code]completed[/code] signal will be emitted automatically when the function returns. It can, therefore, be used as the [code]signal[/code] parameter of the [code]yield[/code] method to resume. - In order to yield on a function, the resulting function should also return a [code]GDScriptFunctionState[/code]. Notice [code]yield(get_tree(), "idle_frame")[/code] from the above example. + [b]Note:[/b] If [code]min[/code] is [code]0[/code], this is equivalent to [method posmod], so prefer using that instead. + [code]wrapi[/code] is more flexible than using the [method posmod] approach by giving the user control over the minimum value. </description> </method> </methods> <constants> <constant name="PI" value="3.141593"> - Constant that represents how many times the diameter of a circle fits around its perimeter. + Constant that represents how many times the diameter of a circle fits around its perimeter. This is equivalent to [code]TAU / 2[/code]. </constant> <constant name="TAU" value="6.283185"> - The circle constant, the circumference of the unit circle. + The circle constant, the circumference of the unit circle in radians. </constant> <constant name="INF" value="inf"> - A positive infinity. (For negative infinity, use -INF). + Positive infinity. For negative infinity, use -INF. </constant> <constant name="NAN" value="nan"> - Macro constant that expands to an expression of type float that represents a NaN. - The NaN values are used to identify undefined or non-representable values for floating-point elements, such as the square root of negative numbers or the result of 0/0. + "Not a Number", an invalid value. [code]NaN[/code] has special properties, including that it is not equal to itself. It is output by some invalid operations, such as dividing zero by zero. </constant> </constants> </class> diff --git a/modules/gdscript/doc_classes/GDScriptFunctionState.xml b/modules/gdscript/doc_classes/GDScriptFunctionState.xml index 9a73764646..5e369b32d9 100644 --- a/modules/gdscript/doc_classes/GDScriptFunctionState.xml +++ b/modules/gdscript/doc_classes/GDScriptFunctionState.xml @@ -4,7 +4,8 @@ State of a function call after yielding. </brief_description> <description> - Calling [method @GDScript.yield] within a function will cause that function to yield and return its current state as an object of this type. The yielded function call can then be resumed later by calling [method resume] on this state object. + FIXME: Outdated docs as of GDScript rewrite in 4.0. + Calling [code]yield[/code] within a function will cause that function to yield and return its current state as an object of this type. The yielded function call can then be resumed later by calling [method resume] on this state object. </description> <tutorials> </tutorials> @@ -26,7 +27,7 @@ </argument> <description> Resume execution of the yielded function call. - If handed an argument, return the argument from the [method @GDScript.yield] call in the yielded function call. You can pass e.g. an [Array] to hand multiple arguments. + If handed an argument, return the argument from the [code]yield[/code] call in the yielded function call. You can pass e.g. an [Array] to hand multiple arguments. This function returns what the resumed function call returns, possibly another function state if yielded again. </description> </method> diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index d0f27b632b..ae1f2893f1 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -29,26 +29,14 @@ /*************************************************************************/ #include "gdscript_highlighter.h" +#include "../gdscript.h" #include "../gdscript_tokenizer.h" #include "editor/editor_settings.h" -#include "scene/gui/text_edit.h" - -inline bool _is_symbol(CharType c) { - return is_symbol(c); -} - -static bool _is_text_char(CharType c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; -} static bool _is_char(CharType c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } -static bool _is_number(CharType c) { - return (c >= '0' && c <= '9'); -} - static bool _is_hex_symbol(CharType c) { return ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); } @@ -57,8 +45,8 @@ static bool _is_bin_symbol(CharType c) { return (c == '0' || c == '1'); } -Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_highlighting(int p_line) { - Map<int, TextEdit::HighlighterInfo> color_map; +Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting(int p_line) { + Dictionary color_map; Type next_type = NONE; Type current_type = NONE; @@ -82,36 +70,140 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ Color keyword_color; Color color; - int in_region = text_editor->_is_line_in_region(p_line); - int deregion = 0; + color_region_cache[p_line] = -1; + int in_region = -1; + if (p_line != 0) { + if (!color_region_cache.has(p_line - 1)) { + get_line_syntax_highlighting(p_line - 1); + } + in_region = color_region_cache[p_line - 1]; + } - const Map<int, TextEdit::Text::ColorRegionInfo> cri_map = text_editor->_get_line_color_region_info(p_line); - const String &str = text_editor->get_line(p_line); + const String &str = text_edit->get_line(p_line); + const int line_length = str.length(); Color prev_color; + + if (in_region != -1 && str.length() == 0) { + color_region_cache[p_line] = in_region; + } for (int j = 0; j < str.length(); j++) { - TextEdit::HighlighterInfo highlighter_info; + Dictionary highlighter_info; - if (deregion > 0) { - deregion--; - if (deregion == 0) { - in_region = -1; + color = font_color; + bool is_char = !is_symbol(str[j]); + bool is_a_symbol = is_symbol(str[j]); + bool is_number = (str[j] >= '0' && str[j] <= '9'); + + /* color regions */ + if (is_a_symbol || in_region != -1) { + int from = j; + for (; from < line_length; from++) { + if (str[from] == '\\') { + from++; + continue; + } + break; } - } - if (deregion != 0) { - if (color != prev_color) { - prev_color = color; - highlighter_info.color = color; - color_map[j] = highlighter_info; - } - continue; - } + if (from != line_length) { + /* check if we are in entering a region */ + if (in_region == -1) { + for (int c = 0; c < color_regions.size(); c++) { + /* check there is enough room */ + int chars_left = line_length - from; + int start_key_length = color_regions[c].start_key.length(); + int end_key_length = color_regions[c].end_key.length(); + if (chars_left < start_key_length) { + continue; + } + + /* search the line */ + bool match = true; + const CharType *start_key = color_regions[c].start_key.c_str(); + for (int k = 0; k < start_key_length; k++) { + if (start_key[k] != str[from + k]) { + match = false; + break; + } + } + if (!match) { + continue; + } + in_region = c; + from += start_key_length; + + /* check if it's the whole line */ + if (end_key_length == 0 || color_regions[c].line_only || from + end_key_length > line_length) { + prev_color = color_regions[in_region].color; + highlighter_info["color"] = color_regions[c].color; + color_map[j] = highlighter_info; + + j = line_length; + if (!color_regions[c].line_only) { + color_region_cache[p_line] = c; + } + } + break; + } - color = font_color; + if (j == line_length) { + continue; + } + } + + /* if we are in one find the end key */ + if (in_region != -1) { + /* check there is enough room */ + int chars_left = line_length - from; + int end_key_length = color_regions[in_region].end_key.length(); + if (chars_left < end_key_length) { + continue; + } + + /* search the line */ + int region_end_index = -1; + const CharType *end_key = color_regions[in_region].start_key.c_str(); + for (; from < line_length; from++) { + if (!is_a_symbol) { + continue; + } + + if (str[from] == '\\') { + from++; + continue; + } + + for (int k = 0; k < end_key_length; k++) { + if (end_key[k] == str[from + k]) { + region_end_index = from; + break; + } + } + + if (region_end_index != -1) { + break; + } + } + + prev_color = color_regions[in_region].color; + highlighter_info["color"] = color_regions[in_region].color; + color_map[j] = highlighter_info; + + previous_type = REGION; + previous_text = ""; + previous_column = j; + j = from; + if (region_end_index == -1) { + color_region_cache[p_line] = in_region; + } - bool is_char = _is_text_char(str[j]); - bool is_symbol = _is_symbol(str[j]); - bool is_number = _is_number(str[j]); + in_region = -1; + prev_is_char = false; + prev_is_number = false; + continue; + } + } + } // allow ABCDEF in hex notation if (is_hex_notation && (_is_hex_symbol(str[j]) || is_number)) { @@ -133,7 +225,7 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ // check for dot or underscore or 'x' for hex notation in floating point number or 'e' for scientific notation if ((str[j] == '.' || str[j] == 'x' || str[j] == 'b' || str[j] == '_' || str[j] == 'e') && !in_word && prev_is_number && !is_number) { is_number = true; - is_symbol = false; + is_a_symbol = false; is_char = false; if (str[j] == 'x' && str[j - 1] == '0') { @@ -151,43 +243,26 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ is_number = false; } - if (is_symbol && str[j] != '.' && in_word) { + if (is_a_symbol && str[j] != '.' && in_word) { in_word = false; } - if (is_symbol && cri_map.has(j)) { - const TextEdit::Text::ColorRegionInfo &cri = cri_map[j]; - - if (in_region == -1) { - if (!cri.end) { - in_region = cri.region; - } - } else { - TextEdit::ColorRegion cr = text_editor->_get_color_region(cri.region); - if (in_region == cri.region && !cr.line_only) { //ignore otherwise - if (cri.end || cr.eq) { - deregion = cr.eq ? cr.begin_key.length() : cr.end_key.length(); - } - } - } - } - if (!is_char) { in_keyword = false; } - if (in_region == -1 && !in_keyword && is_char && !prev_is_char) { + if (!in_keyword && is_char && !prev_is_char) { int to = j; - while (to < str.length() && _is_text_char(str[to])) { + while (to < str.length() && !is_symbol(str[to])) { to++; } String word = str.substr(j, to - j); Color col = Color(); - if (text_editor->has_keyword_color(word)) { - col = text_editor->get_keyword_color(word); - } else if (text_editor->has_member_color(word)) { - col = text_editor->get_member_color(word); + if (keywords.has(word)) { + col = keywords[word]; + } else if (member_keywords.has(word)) { + col = member_keywords[word]; for (int k = j - 1; k >= 0; k--) { if (str[k] == '.') { col = Color(); //member indexing not allowed @@ -206,7 +281,7 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ if (!in_function_name && in_word && !in_keyword) { int k = j; - while (k < str.length() && !_is_symbol(str[k]) && str[k] != '\t' && str[k] != ' ') { + while (k < str.length() && !is_symbol(str[k]) && str[k] != '\t' && str[k] != ' ') { k++; } @@ -217,14 +292,14 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ if (str[k] == '(') { in_function_name = true; - } else if (previous_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::TK_PR_VAR)) { + } else if (previous_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::VAR)) { in_variable_declaration = true; } } if (!in_function_name && !in_member_variable && !in_keyword && !is_number && in_word) { int k = j; - while (k > 0 && !_is_symbol(str[k]) && str[k] != '\t' && str[k] != ' ') { + while (k > 0 && !is_symbol(str[k]) && str[k] != '\t' && str[k] != ' ') { k--; } @@ -233,7 +308,7 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ } } - if (is_symbol) { + if (is_a_symbol) { if (in_function_name) { in_function_args = true; } @@ -270,14 +345,11 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ if (!in_node_path && in_region == -1 && str[j] == '$') { in_node_path = true; - } else if (in_region != -1 || (is_symbol && str[j] != '/')) { + } else if (in_region != -1 || (is_a_symbol && str[j] != '/')) { in_node_path = false; } - if (in_region >= 0) { - next_type = REGION; - color = text_editor->_get_color_region(in_region).color; - } else if (in_node_path) { + if (in_node_path) { next_type = NODE_PATH; color = node_path_color; } else if (in_keyword) { @@ -289,12 +361,12 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ } else if (in_function_name) { next_type = FUNCTION; - if (previous_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::TK_PR_FUNCTION)) { + if (previous_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) { color = function_definition_color; } else { color = function_color; } - } else if (is_symbol) { + } else if (is_a_symbol) { next_type = SYMBOL; color = symbol_color; } else if (is_number) { @@ -335,32 +407,133 @@ Map<int, TextEdit::HighlighterInfo> GDScriptSyntaxHighlighter::_get_line_syntax_ if (color != prev_color) { prev_color = color; - highlighter_info.color = color; + highlighter_info["color"] = color; color_map[j] = highlighter_info; } } return color_map; } -String GDScriptSyntaxHighlighter::get_name() const { +String GDScriptSyntaxHighlighter::_get_name() const { return "GDScript"; } -List<String> GDScriptSyntaxHighlighter::get_supported_languages() { - List<String> languages; +Array GDScriptSyntaxHighlighter::_get_supported_languages() const { + Array languages; languages.push_back("GDScript"); return languages; } void GDScriptSyntaxHighlighter::_update_cache() { - font_color = text_editor->get_theme_color("font_color"); - symbol_color = text_editor->get_theme_color("symbol_color"); - function_color = text_editor->get_theme_color("function_color"); - number_color = text_editor->get_theme_color("number_color"); - member_color = text_editor->get_theme_color("member_variable_color"); + keywords.clear(); + member_keywords.clear(); + color_regions.clear(); + color_region_cache.clear(); + + font_color = text_edit->get_theme_color("font_color"); + symbol_color = EDITOR_GET("text_editor/highlighting/symbol_color"); + function_color = EDITOR_GET("text_editor/highlighting/function_color"); + number_color = EDITOR_GET("text_editor/highlighting/number_color"); + member_color = EDITOR_GET("text_editor/highlighting/member_variable_color"); + + /* Engine types. */ + const Color types_color = EDITOR_GET("text_editor/highlighting/engine_type_color"); + List<StringName> types; + ClassDB::get_class_list(&types); + for (List<StringName>::Element *E = types.front(); E; E = E->next()) { + String n = E->get(); + if (n.begins_with("_")) { + n = n.substr(1, n.length()); + } + keywords[n] = types_color; + } + + /* User types. */ + const Color usertype_color = EDITOR_GET("text_editor/highlighting/user_type_color"); + List<StringName> global_classes; + ScriptServer::get_global_class_list(&global_classes); + for (List<StringName>::Element *E = global_classes.front(); E; E = E->next()) { + keywords[String(E->get())] = usertype_color; + } + + /* Autoloads. */ + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); + for (Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E; E = E->next()) { + const ProjectSettings::AutoloadInfo &info = E->value(); + if (info.is_singleton) { + keywords[info.name] = usertype_color; + } + } + + const GDScriptLanguage *gdscript = GDScriptLanguage::get_singleton(); + + /* Core types. */ + const Color basetype_color = EDITOR_GET("text_editor/highlighting/base_type_color"); + List<String> core_types; + gdscript->get_core_type_words(&core_types); + for (List<String>::Element *E = core_types.front(); E; E = E->next()) { + keywords[E->get()] = basetype_color; + } + + /* Reserved words. */ + const Color keyword_color = EDITOR_GET("text_editor/highlighting/keyword_color"); + List<String> keyword_list; + gdscript->get_reserved_words(&keyword_list); + for (List<String>::Element *E = keyword_list.front(); E; E = E->next()) { + keywords[E->get()] = keyword_color; + } + + /* Comments */ + const Color comment_color = EDITOR_GET("text_editor/highlighting/comment_color"); + List<String> comments; + gdscript->get_comment_delimiters(&comments); + for (List<String>::Element *E = comments.front(); E; E = E->next()) { + String comment = E->get(); + String beg = comment.get_slice(" ", 0); + String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String(); + add_color_region(beg, end, comment_color, end == ""); + } + + /* Strings */ + const Color string_color = EDITOR_GET("text_editor/highlighting/string_color"); + List<String> strings; + gdscript->get_string_delimiters(&strings); + for (List<String>::Element *E = strings.front(); E; E = E->next()) { + String string = E->get(); + String beg = string.get_slice(" ", 0); + String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 1) : String(); + add_color_region(beg, end, string_color, end == ""); + } + + const Ref<Script> script = _get_edited_resource(); + if (script.is_valid()) { + /* Member types. */ + const Color member_variable_color = EDITOR_GET("text_editor/highlighting/member_variable_color"); + StringName instance_base = script->get_instance_base_type(); + if (instance_base != StringName()) { + List<PropertyInfo> plist; + ClassDB::get_property_list(instance_base, &plist); + for (List<PropertyInfo>::Element *E = plist.front(); E; E = E->next()) { + String name = E->get().name; + if (E->get().usage & PROPERTY_USAGE_CATEGORY || E->get().usage & PROPERTY_USAGE_GROUP || E->get().usage & PROPERTY_USAGE_SUBGROUP) { + continue; + } + if (name.find("/") != -1) { + continue; + } + member_keywords[name] = member_variable_color; + } + + List<String> clist; + ClassDB::get_integer_constant_list(instance_base, &clist); + for (List<String>::Element *E = clist.front(); E; E = E->next()) { + member_keywords[E->get()] = member_variable_color; + } + } + } - const String text_editor_color_theme = EditorSettings::get_singleton()->get("text_editor/theme/color_theme"); - const bool default_theme = text_editor_color_theme == "Default"; + const String text_edit_color_theme = EditorSettings::get_singleton()->get("text_editor/theme/color_theme"); + const bool default_theme = text_edit_color_theme == "Default"; if (default_theme || EditorSettings::get_singleton()->is_dark_theme()) { function_definition_color = Color(0.4, 0.9, 1.0); @@ -372,7 +545,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { EDITOR_DEF("text_editor/highlighting/gdscript/function_definition_color", function_definition_color); EDITOR_DEF("text_editor/highlighting/gdscript/node_path_color", node_path_color); - if (text_editor_color_theme == "Adaptive" || default_theme) { + if (text_edit_color_theme == "Adaptive" || default_theme) { EditorSettings::get_singleton()->set_initial_value( "text_editor/highlighting/gdscript/function_definition_color", function_definition_color, @@ -388,6 +561,31 @@ void GDScriptSyntaxHighlighter::_update_cache() { type_color = EDITOR_GET("text_editor/highlighting/base_type_color"); } -SyntaxHighlighter *GDScriptSyntaxHighlighter::create() { - return memnew(GDScriptSyntaxHighlighter); +void GDScriptSyntaxHighlighter::add_color_region(const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only) { + for (int i = 0; i < p_start_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_start_key[i]), "color regions must start with a symbol"); + } + + if (p_end_key.length() > 0) { + for (int i = 0; i < p_end_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_end_key[i]), "color regions must end with a symbol"); + } + } + + for (int i = 0; i < color_regions.size(); i++) { + ERR_FAIL_COND_MSG(color_regions[i].start_key == p_start_key, "color region with start key '" + p_start_key + "' already exists."); + } + + ColorRegion color_region; + color_region.color = p_color; + color_region.start_key = p_start_key; + color_region.end_key = p_end_key; + color_region.line_only = p_line_only; + color_regions.push_back(color_region); +} + +Ref<EditorSyntaxHighlighter> GDScriptSyntaxHighlighter::_create() const { + Ref<GDScriptSyntaxHighlighter> syntax_highlighter; + syntax_highlighter.instance(); + return syntax_highlighter; } diff --git a/modules/gdscript/editor/gdscript_highlighter.h b/modules/gdscript/editor/gdscript_highlighter.h index e652fb1471..49357f3d2e 100644 --- a/modules/gdscript/editor/gdscript_highlighter.h +++ b/modules/gdscript/editor/gdscript_highlighter.h @@ -31,10 +31,25 @@ #ifndef GDSCRIPT_HIGHLIGHTER_H #define GDSCRIPT_HIGHLIGHTER_H +#include "editor/plugins/script_editor_plugin.h" #include "scene/gui/text_edit.h" -class GDScriptSyntaxHighlighter : public SyntaxHighlighter { +class GDScriptSyntaxHighlighter : public EditorSyntaxHighlighter { + GDCLASS(GDScriptSyntaxHighlighter, EditorSyntaxHighlighter) + private: + struct ColorRegion { + Color color; + String start_key; + String end_key; + bool line_only; + }; + Vector<ColorRegion> color_regions; + Map<int, int> color_region_cache; + + Dictionary keywords; + Dictionary member_keywords; + enum Type { NONE, REGION, @@ -59,14 +74,16 @@ private: Color node_path_color; Color type_color; + void add_color_region(const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only = false); + public: - static SyntaxHighlighter *create(); + virtual void _update_cache() override; + virtual Dictionary _get_line_syntax_highlighting(int p_line) override; - virtual void _update_cache(); - virtual Map<int, TextEdit::HighlighterInfo> _get_line_syntax_highlighting(int p_line); + virtual String _get_name() const override; + virtual Array _get_supported_languages() const override; - virtual String get_name() const; - virtual List<String> get_supported_languages(); + virtual Ref<EditorSyntaxHighlighter> _create() const override; }; #endif // GDSCRIPT_HIGHLIGHTER_H diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp index 6d454e43f2..944ed859f5 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp @@ -37,9 +37,11 @@ void GDScriptEditorTranslationParserPlugin::get_recognized_extensions(List<Strin GDScriptLanguage::get_singleton()->get_recognized_extensions(r_extensions); } -Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) { - // Parse and match all GDScript function API that involves translation string. - // E.g get_node("Label").text = "something", var test = tr("something"), "something" will be matched and collected. +Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) { + // Extract all translatable strings using the parsed tree from GDSriptParser. + // The strategy is to find all ExpressionNode and AssignmentNode from the tree and extract strings if relevant, i.e + // Search strings in ExpressionNode -> CallNode -> tr(), set_text(), set_placeholder() etc. + // Search strings in AssignmentNode -> text = "__", hint_tooltip = "__" etc. Error err; RES loaded_res = ResourceLoader::load(p_path, "", false, &err); @@ -48,108 +50,302 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve return err; } + ids = r_ids; + ids_ctx_plural = r_ids_ctx_plural; Ref<GDScript> gdscript = loaded_res; String source_code = gdscript->get_source_code(); - Vector<String> parsed_strings; - - // Search translation strings with RegEx. - regex.clear(); - regex.compile(String("|").join(patterns)); - Array results = regex.search_all(source_code); - _get_captured_strings(results, &parsed_strings); - - // Special handling for FileDialog. - Vector<String> temp; - _parse_file_dialog(source_code, &temp); - parsed_strings.append_array(temp); - - // Filter out / and + - String filter = "(?:\\\\\\n|\"[\\s\\\\]*\\+\\s*\")"; - regex.clear(); - regex.compile(filter); - for (int i = 0; i < parsed_strings.size(); i++) { - parsed_strings.set(i, regex.sub(parsed_strings[i], "", true)); + + GDScriptParser parser; + err = parser.parse(source_code, p_path, false); + if (err != OK) { + ERR_PRINT("Failed to parse with GDScript with GDScriptParser."); + return err; } - r_extracted_strings->append_array(parsed_strings); + // Traverse through the parsed tree from GDScriptParser. + GDScriptParser::ClassNode *c = parser.get_tree(); + _traverse_class(c); return OK; } -void GDScriptEditorTranslationParserPlugin::_parse_file_dialog(const String &p_source_code, Vector<String> *r_output) { - // FileDialog API has the form .filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]). - // First filter: Get "*.png ; PNG Images", "*.gd ; GDScript Files" from PackedStringArray. - regex.clear(); - regex.compile(String("|").join(file_dialog_patterns)); - Array results = regex.search_all(p_source_code); - - Vector<String> temp; - _get_captured_strings(results, &temp); - String captured_strings = String(",").join(temp); - - // Second filter: Get the texts after semicolon from "*.png ; PNG Images","*.gd ; GDScript Files". - String second_filter = "\"[^;]+;" + text + "\""; - regex.clear(); - regex.compile(second_filter); - results = regex.search_all(captured_strings); - _get_captured_strings(results, r_output); - for (int i = 0; i < r_output->size(); i++) { - r_output->set(i, r_output->get(i).strip_edges()); +void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) { + for (int i = 0; i < p_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &m = p_class->members[i]; + // There are 7 types of Member, but only class, function and variable can contain translatable strings. + switch (m.type) { + case GDScriptParser::ClassNode::Member::CLASS: + _traverse_class(m.m_class); + break; + case GDScriptParser::ClassNode::Member::FUNCTION: + _traverse_function(m.function); + break; + case GDScriptParser::ClassNode::Member::VARIABLE: + _read_variable(m.variable); + break; + default: + break; + } + } +} + +void GDScriptEditorTranslationParserPlugin::_traverse_function(const GDScriptParser::FunctionNode *p_func) { + _traverse_block(p_func->body); +} + +void GDScriptEditorTranslationParserPlugin::_read_variable(const GDScriptParser::VariableNode *p_var) { + _assess_expression(p_var->initializer); +} + +void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser::SuiteNode *p_suite) { + if (!p_suite) { + return; + } + + const Vector<GDScriptParser::Node *> &statements = p_suite->statements; + for (int i = 0; i < statements.size(); i++) { + GDScriptParser::Node *statement = statements[i]; + + // Statements with Node type constant, break, continue, pass, breakpoint are skipped because they can't contain translatable strings. + switch (statement->type) { + case GDScriptParser::Node::VARIABLE: + _assess_expression(static_cast<GDScriptParser::VariableNode *>(statement)->initializer); + break; + case GDScriptParser::Node::IF: { + GDScriptParser::IfNode *if_node = static_cast<GDScriptParser::IfNode *>(statement); + _assess_expression(if_node->condition); + //FIXME : if the elif logic is changed in GDScriptParser, then this probably will have to change as well. See GDScriptParser::TreePrinter::print_if(). + _traverse_block(if_node->true_block); + _traverse_block(if_node->false_block); + break; + } + case GDScriptParser::Node::FOR: { + GDScriptParser::ForNode *for_node = static_cast<GDScriptParser::ForNode *>(statement); + _assess_expression(for_node->list); + _traverse_block(for_node->loop); + break; + } + case GDScriptParser::Node::WHILE: { + GDScriptParser::WhileNode *while_node = static_cast<GDScriptParser::WhileNode *>(statement); + _assess_expression(while_node->condition); + _traverse_block(while_node->loop); + break; + } + case GDScriptParser::Node::MATCH: { + GDScriptParser::MatchNode *match_node = static_cast<GDScriptParser::MatchNode *>(statement); + _assess_expression(match_node->test); + for (int j = 0; j < match_node->branches.size(); j++) { + _traverse_block(match_node->branches[j]->block); + } + break; + } + case GDScriptParser::Node::RETURN: + _assess_expression(static_cast<GDScriptParser::ReturnNode *>(statement)->return_value); + break; + case GDScriptParser::Node::ASSERT: + _assess_expression((static_cast<GDScriptParser::AssertNode *>(statement))->condition); + break; + case GDScriptParser::Node::ASSIGNMENT: + _assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(statement)); + break; + default: + if (statement->is_expression()) { + _assess_expression(static_cast<GDScriptParser::ExpressionNode *>(statement)); + } + break; + } + } +} + +void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::ExpressionNode *p_expression) { + // Explore all ExpressionNodes to find CallNodes which contain translation strings, such as tr(), set_text() etc. + // tr() can be embedded quite deep within multiple ExpressionNodes so need to dig down to search through all ExpressionNodes. + if (!p_expression) { + return; + } + + // ExpressionNode of type await, cast, get_node, identifier, literal, preload, self, subscript, unary are ignored as they can't be CallNode + // containing translation strings. + switch (p_expression->type) { + case GDScriptParser::Node::ARRAY: { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(p_expression); + for (int i = 0; i < array_node->elements.size(); i++) { + _assess_expression(array_node->elements[i]); + } + break; + } + case GDScriptParser::Node::ASSIGNMENT: + _assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(p_expression)); + break; + case GDScriptParser::Node::BINARY_OPERATOR: { + GDScriptParser::BinaryOpNode *binary_op_node = static_cast<GDScriptParser::BinaryOpNode *>(p_expression); + _assess_expression(binary_op_node->left_operand); + _assess_expression(binary_op_node->right_operand); + break; + } + case GDScriptParser::Node::CALL: { + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_expression); + _extract_from_call(call_node); + for (int i = 0; i < call_node->arguments.size(); i++) { + _assess_expression(call_node->arguments[i]); + } + } break; + case GDScriptParser::Node::DICTIONARY: { + GDScriptParser::DictionaryNode *dict_node = static_cast<GDScriptParser::DictionaryNode *>(p_expression); + for (int i = 0; i < dict_node->elements.size(); i++) { + _assess_expression(dict_node->elements[i].key); + _assess_expression(dict_node->elements[i].value); + } + break; + } + case GDScriptParser::Node::TERNARY_OPERATOR: { + GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<GDScriptParser::TernaryOpNode *>(p_expression); + _assess_expression(ternary_op_node->condition); + _assess_expression(ternary_op_node->true_expr); + _assess_expression(ternary_op_node->false_expr); + break; + } + default: + break; + } +} + +void GDScriptEditorTranslationParserPlugin::_assess_assignment(GDScriptParser::AssignmentNode *p_assignment) { + // Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____" + + StringName assignee_name; + if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) { + assignee_name = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name; + } else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) { + assignee_name = static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee)->attribute->name; + } + + if (assignment_patterns.has(assignee_name) && p_assignment->assigned_value->type == GDScriptParser::Node::LITERAL) { + // If the assignment is towards one of the extract patterns (text, hint_tooltip etc.), and the value is a string literal, we collect the string. + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_assignment->assigned_value)->value); + } else if (assignee_name == fd_filters && p_assignment->assigned_value->type == GDScriptParser::Node::CALL) { + // FileDialog.filters accepts assignment in the form of PackedStringArray. For example, + // get_node("FileDialog").filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]). + + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_assignment->assigned_value); + if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]); + + // Extract the name in "extension ; name" of PackedStringArray. + for (int i = 0; i < array_node->elements.size(); i++) { + _extract_fd_literals(array_node->elements[i]); + } + } + } else { + // If the assignee is not in extract patterns or the assigned_value is not Literal type, try to see if the assigned_value contains tr(). + _assess_expression(p_assignment->assigned_value); } } -void GDScriptEditorTranslationParserPlugin::_get_captured_strings(const Array &p_results, Vector<String> *r_output) { - Ref<RegExMatch> result; - for (int i = 0; i < p_results.size(); i++) { - result = p_results[i]; - for (int j = 0; j < result->get_group_count(); j++) { - String s = result->get_string(j + 1); - // Prevent reading text with only spaces. - if (!s.strip_edges().empty()) { - r_output->push_back(s); +void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::CallNode *p_call) { + // Extract the translatable strings coming from function calls. For example: + // tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____"). + + StringName function_name = p_call->function_name; + + // Variables for extracting tr() and tr_n(). + Vector<String> id_ctx_plural; + id_ctx_plural.resize(3); + bool extract_id_ctx_plural = true; + + if (function_name == tr_func) { + // Extract from tr(id, ctx). + for (int i = 0; i < p_call->arguments.size(); i++) { + if (p_call->arguments[i]->type == GDScriptParser::Node::LITERAL) { + id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[i])->value; + } else { + // Avoid adding something like tr("Flying dragon", var_context_level_1). We want to extract both id and context together. + extract_id_ctx_plural = false; + } + } + if (extract_id_ctx_plural) { + ids_ctx_plural->push_back(id_ctx_plural); + } + } else if (function_name == trn_func) { + // Extract from tr_n(id, plural, n, ctx). + Vector<int> indices; + indices.push_back(0); + indices.push_back(3); + indices.push_back(1); + for (int i = 0; i < indices.size(); i++) { + if (indices[i] >= p_call->arguments.size()) { + continue; + } + + if (p_call->arguments[indices[i]]->type == GDScriptParser::Node::LITERAL) { + id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[indices[i]])->value; + } else { + extract_id_ctx_plural = false; + } + } + if (extract_id_ctx_plural) { + ids_ctx_plural->push_back(id_ctx_plural); + } + } else if (first_arg_patterns.has(function_name)) { + // Extracting argument with only string literals. In other words, not extracting something like set_text("hello " + some_var). + if (p_call->arguments[0]->type == GDScriptParser::Node::LITERAL) { + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[0])->value); + } + } else if (second_arg_patterns.has(function_name)) { + if (p_call->arguments[1]->type == GDScriptParser::Node::LITERAL) { + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[1])->value); + } + } else if (function_name == fd_add_filter) { + // Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images"). + _extract_fd_literals(p_call->arguments[0]); + + } else if (function_name == fd_set_filter && p_call->arguments[0]->type == GDScriptParser::Node::CALL) { + // FileDialog.set_filters() accepts assignment in the form of PackedStringArray. For example, + // get_node("FileDialog").set_filters( PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])). + + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_call->arguments[0]); + if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]); + for (int i = 0; i < array_node->elements.size(); i++) { + _extract_fd_literals(array_node->elements[i]); } } } } +void GDScriptEditorTranslationParserPlugin::_extract_fd_literals(GDScriptParser::ExpressionNode *p_expression) { + // Extract the name in "extension ; name". + + if (p_expression->type == GDScriptParser::Node::LITERAL) { + String arg_val = String(static_cast<GDScriptParser::LiteralNode *>(p_expression)->value); + PackedStringArray arr = arg_val.split(";", true); + if (arr.size() != 2) { + ERR_PRINT("Argument for setting FileDialog has bad format."); + return; + } + ids->push_back(arr[1].strip_edges()); + } +} + GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() { - // Regex search pattern templates. - // The extra complication in the regex pattern is to ensure that the matching works when users write over multiple lines, use tabs etc. - const String dot = "\\.[\\s\\\\]*"; - const String str_assign_template = "[\\s\\\\]*=[\\s\\\\]*\"" + text + "\""; - const String first_arg_template = "[\\s\\\\]*\\([\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)"; - const String second_arg_template = "[\\s\\\\]*\\([\\s\\S]+?,[\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)"; - - // Common patterns. - patterns.push_back("tr" + first_arg_template); - patterns.push_back(dot + "text" + str_assign_template); - patterns.push_back(dot + "placeholder_text" + str_assign_template); - patterns.push_back(dot + "hint_tooltip" + str_assign_template); - patterns.push_back(dot + "set_text" + first_arg_template); - patterns.push_back(dot + "set_tooltip" + first_arg_template); - patterns.push_back(dot + "set_placeholder" + first_arg_template); - - // Tabs and TabContainer API. - patterns.push_back(dot + "set_tab_title" + second_arg_template); - patterns.push_back(dot + "add_tab" + first_arg_template); - - // PopupMenu API. - patterns.push_back(dot + "add_check_item" + first_arg_template); - patterns.push_back(dot + "add_icon_check_item" + second_arg_template); - patterns.push_back(dot + "add_icon_item" + second_arg_template); - patterns.push_back(dot + "add_icon_radio_check_item" + second_arg_template); - patterns.push_back(dot + "add_item" + first_arg_template); - patterns.push_back(dot + "add_multistate_item" + first_arg_template); - patterns.push_back(dot + "add_radio_check_item" + first_arg_template); - patterns.push_back(dot + "add_separator" + first_arg_template); - patterns.push_back(dot + "add_submenu_item" + first_arg_template); - patterns.push_back(dot + "set_item_text" + second_arg_template); - //patterns.push_back(dot + "set_item_tooltip" + second_arg_template); //no tr() behind this function. might be bug. - - // FileDialog API - special case. - const String fd_text = "((?:[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*\"[\\s\\\\]*,?)*)"; - const String packed_string_array = "[\\s\\\\]*PackedStringArray[\\s\\\\]*\\([\\s\\\\]*\\[" + fd_text + "\\][\\s\\\\]*\\)"; - file_dialog_patterns.push_back(dot + "add_filter[\\s\\\\]*\\(" + fd_text + "[\\s\\\\]*\\)"); - file_dialog_patterns.push_back(dot + "filters[\\s\\\\]*=" + packed_string_array); - file_dialog_patterns.push_back(dot + "set_filters[\\s\\\\]*\\(" + packed_string_array + "[\\s\\\\]*\\)"); + assignment_patterns.insert("text"); + assignment_patterns.insert("placeholder_text"); + assignment_patterns.insert("hint_tooltip"); + + first_arg_patterns.insert("set_text"); + first_arg_patterns.insert("set_tooltip"); + first_arg_patterns.insert("set_placeholder"); + first_arg_patterns.insert("add_tab"); + first_arg_patterns.insert("add_check_item"); + first_arg_patterns.insert("add_item"); + first_arg_patterns.insert("add_multistate_item"); + first_arg_patterns.insert("add_radio_check_item"); + first_arg_patterns.insert("add_separator"); + first_arg_patterns.insert("add_submenu_item"); + + second_arg_patterns.insert("set_tab_title"); + second_arg_patterns.insert("add_icon_check_item"); + second_arg_patterns.insert("add_icon_item"); + second_arg_patterns.insert("add_icon_radio_check_item"); + second_arg_patterns.insert("set_item_text"); } diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.h b/modules/gdscript/editor/gdscript_translation_parser_plugin.h index 9fa4b69f01..5ea416d4cc 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.h +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.h @@ -31,23 +31,40 @@ #ifndef GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H #define GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H +#include "core/set.h" #include "editor/editor_translation_parser.h" +#include "modules/gdscript/gdscript_parser.h" #include "modules/regex/regex.h" class GDScriptEditorTranslationParserPlugin : public EditorTranslationParserPlugin { GDCLASS(GDScriptEditorTranslationParserPlugin, EditorTranslationParserPlugin); - // Regex and search patterns that are used to match translation strings. - const String text = "((?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*)"; - RegEx regex; - Vector<String> patterns; - Vector<String> file_dialog_patterns; + Vector<String> *ids; + Vector<Vector<String>> *ids_ctx_plural; - void _parse_file_dialog(const String &p_source_code, Vector<String> *r_output); - void _get_captured_strings(const Array &p_results, Vector<String> *r_output); + // List of patterns used for extracting translation strings. + StringName tr_func = "tr"; + StringName trn_func = "tr_n"; + Set<StringName> assignment_patterns; + Set<StringName> first_arg_patterns; + Set<StringName> second_arg_patterns; + // FileDialog patterns. + StringName fd_add_filter = "add_filter"; + StringName fd_set_filter = "set_filters"; + StringName fd_filters = "filters"; + + void _traverse_class(const GDScriptParser::ClassNode *p_class); + void _traverse_function(const GDScriptParser::FunctionNode *p_func); + void _traverse_block(const GDScriptParser::SuiteNode *p_suite); + + void _read_variable(const GDScriptParser::VariableNode *p_var); + void _assess_expression(GDScriptParser::ExpressionNode *p_expression); + void _assess_assignment(GDScriptParser::AssignmentNode *p_assignment); + void _extract_from_call(GDScriptParser::CallNode *p_call); + void _extract_fd_literals(GDScriptParser::ExpressionNode *p_expression); public: - virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings) override; + virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override; virtual void get_recognized_extensions(List<String> *r_extensions) const override; GDScriptEditorTranslationParserPlugin(); diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 01af562327..541d46a2af 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -39,7 +39,11 @@ #include "core/os/file_access.h" #include "core/os/os.h" #include "core/project_settings.h" +#include "gdscript_analyzer.h" +#include "gdscript_cache.h" #include "gdscript_compiler.h" +#include "gdscript_parser.h" +#include "gdscript_warning.h" /////////////////////////// @@ -79,6 +83,17 @@ Object *GDScriptNativeClass::instance() { return ClassDB::instance(name); } +void GDScript::_super_implicit_constructor(GDScript *p_script, GDScriptInstance *p_instance, Callable::CallError &r_error) { + GDScript *base = p_script->_base; + if (base != nullptr) { + _super_implicit_constructor(base, p_instance, r_error); + if (r_error.error != Callable::CallError::CALL_OK) { + return; + } + } + p_script->implicit_initializer->call(p_instance, nullptr, 0, r_error); +} + GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_isref, Callable::CallError &r_error) { /* STEP 1, CREATE */ @@ -101,10 +116,8 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco MutexLock lock(GDScriptLanguage::singleton->lock); instances.insert(instance->owner); } - if (p_argcount < 0) { - return instance; - } - initializer->call(instance, p_args, p_argcount, r_error); + + _super_implicit_constructor(this, instance, r_error); if (r_error.error != Callable::CallError::CALL_OK) { instance->script = Ref<GDScript>(); instance->owner->set_script_instance(nullptr); @@ -114,6 +127,22 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco } ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance."); } + + if (p_argcount < 0) { + return instance; + } + if (initializer != nullptr) { + initializer->call(instance, p_args, p_argcount, r_error); + if (r_error.error != Callable::CallError::CALL_OK) { + instance->script = Ref<GDScript>(); + instance->owner->set_script_instance(nullptr); + { + MutexLock lock(GDScriptLanguage::singleton->lock); + instances.erase(p_owner); + } + ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance."); + } + } //@TODO make thread safe return instance; } @@ -382,13 +411,11 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call) { } GDScriptParser parser; - Error err = parser.parse(source, basedir, true, path); - - if (err == OK) { - const GDScriptParser::Node *root = parser.get_parse_tree(); - ERR_FAIL_COND_V(root->type != GDScriptParser::Node::TYPE_CLASS, false); + GDScriptAnalyzer analyzer(&parser); + Error err = parser.parse(source, path, false); - const GDScriptParser::ClassNode *c = static_cast<const GDScriptParser::ClassNode *>(root); + if (err == OK && analyzer.analyze() == OK) { + const GDScriptParser::ClassNode *c = parser.get_tree(); if (base_cache.is_valid()) { base_cache->inheriters_cache.erase(get_instance_id()); @@ -397,8 +424,8 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call) { if (c->extends_used) { String path = ""; - if (String(c->extends_file) != "" && String(c->extends_file) != get_path()) { - path = c->extends_file; + if (String(c->extends_path) != "" && String(c->extends_path) != get_path()) { + path = c->extends_path; if (path.is_rel_path()) { String base = get_path(); if (base == "" || base.is_rel_path()) { @@ -407,8 +434,8 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call) { path = base.get_base_dir().plus_file(path); } } - } else if (c->extends_class.size() != 0) { - String base = c->extends_class[0]; + } else if (c->extends.size() != 0) { + const StringName &base = c->extends[0]; if (ScriptServer::is_global_class(base)) { path = ScriptServer::get_global_class_path(base); @@ -431,20 +458,36 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call) { members_cache.clear(); member_default_values_cache.clear(); + _signals.clear(); - for (int i = 0; i < c->variables.size(); i++) { - if (c->variables[i]._export.type == Variant::NIL) { - continue; - } - - members_cache.push_back(c->variables[i]._export); - member_default_values_cache[c->variables[i].identifier] = c->variables[i].default_value; - } + for (int i = 0; i < c->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = c->members[i]; - _signals.clear(); + switch (member.type) { + case GDScriptParser::ClassNode::Member::VARIABLE: { + if (!member.variable->exported) { + continue; + } - for (int i = 0; i < c->_signals.size(); i++) { - _signals[c->_signals[i].name] = c->_signals[i].arguments; + members_cache.push_back(member.variable->export_info); + Variant default_value; + if (member.variable->initializer && member.variable->initializer->is_constant) { + default_value = member.variable->initializer->reduced_value; + } + member_default_values_cache[member.variable->identifier->name] = default_value; + } break; + case GDScriptParser::ClassNode::Member::SIGNAL: { + // TODO: Cache this in parser to avoid loops like this. + Vector<StringName> parameters_names; + parameters_names.resize(member.signal->parameters.size()); + for (int j = 0; j < member.signal->parameters.size(); j++) { + parameters_names.write[j] = member.signal->parameters[j]->identifier->name; + } + _signals[member.signal->identifier->name] = parameters_names; + } break; + default: + break; // Nothing. + } } } else { placeholder_fallback_enabled = true; @@ -553,18 +596,46 @@ Error GDScript::reload(bool p_keep_state) { return OK; } + { + String source_path = path; + if (source_path.empty()) { + source_path = get_path(); + } + if (!source_path.empty()) { + MutexLock lock(GDScriptCache::singleton->lock); + Ref<GDScript> self(this); + if (!GDScriptCache::singleton->shallow_gdscript_cache.has(source_path)) { + GDScriptCache::singleton->shallow_gdscript_cache[source_path] = self; + self->unreference(); + } + } + } + valid = false; GDScriptParser parser; - Error err = parser.parse(source, basedir, false, path); + Error err = parser.parse(source, path, false); + if (err) { + if (EngineDebugger::is_active()) { + GDScriptLanguage::get_singleton()->debug_break_parse(get_path(), parser.get_errors().front()->get().line, "Parser Error: " + parser.get_errors().front()->get().message); + } + // TODO: Show all error messages. + _err_print_error("GDScript::reload", path.empty() ? "built-in" : (const char *)path.utf8().get_data(), parser.get_errors().front()->get().line, ("Parse Error: " + parser.get_errors().front()->get().message).utf8().get_data(), ERR_HANDLER_SCRIPT); + ERR_FAIL_V(ERR_PARSE_ERROR); + } + + GDScriptAnalyzer analyzer(&parser); + err = analyzer.analyze(); + if (err) { if (EngineDebugger::is_active()) { - GDScriptLanguage::get_singleton()->debug_break_parse(get_path(), parser.get_error_line(), "Parser Error: " + parser.get_error()); + GDScriptLanguage::get_singleton()->debug_break_parse(get_path(), parser.get_errors().front()->get().line, "Parser Error: " + parser.get_errors().front()->get().message); } - _err_print_error("GDScript::reload", path.empty() ? "built-in" : (const char *)path.utf8().get_data(), parser.get_error_line(), ("Parse Error: " + parser.get_error()).utf8().get_data(), ERR_HANDLER_SCRIPT); + // TODO: Show all error messages. + _err_print_error("GDScript::reload", path.empty() ? "built-in" : (const char *)path.utf8().get_data(), parser.get_errors().front()->get().line, ("Parse Error: " + parser.get_errors().front()->get().message).utf8().get_data(), ERR_HANDLER_SCRIPT); ERR_FAIL_V(ERR_PARSE_ERROR); } - bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool_script(); + bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); GDScriptCompiler compiler; err = compiler.compile(&parser, this, p_keep_state); @@ -585,7 +656,7 @@ Error GDScript::reload(bool p_keep_state) { const GDScriptWarning &warning = E->get(); if (EngineDebugger::is_active()) { Vector<ScriptLanguage::StackInfo> si; - EngineDebugger::get_script_debugger()->send_error("", get_path(), warning.line, warning.get_name(), warning.get_message(), ERR_HANDLER_WARNING, si); + EngineDebugger::get_script_debugger()->send_error("", get_path(), warning.start_line, warning.get_name(), warning.get_message(), ERR_HANDLER_WARNING, si); } } #endif @@ -753,83 +824,12 @@ void GDScript::_bind_methods() { } Vector<uint8_t> GDScript::get_as_byte_code() const { - GDScriptTokenizerBuffer tokenizer; - return tokenizer.parse_code_string(source); + return Vector<uint8_t>(); }; +// TODO: Fully remove this. There's not this kind of "bytecode" anymore. Error GDScript::load_byte_code(const String &p_path) { - Vector<uint8_t> bytecode; - - if (p_path.ends_with("gde")) { - FileAccess *fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V(!fa, ERR_CANT_OPEN); - - FileAccessEncrypted *fae = memnew(FileAccessEncrypted); - ERR_FAIL_COND_V(!fae, ERR_CANT_OPEN); - - Vector<uint8_t> key; - key.resize(32); - for (int i = 0; i < key.size(); i++) { - key.write[i] = script_encryption_key[i]; - } - - Error err = fae->open_and_parse(fa, key, FileAccessEncrypted::MODE_READ); - - if (err) { - fa->close(); - memdelete(fa); - memdelete(fae); - - ERR_FAIL_COND_V(err, err); - } - - bytecode.resize(fae->get_len()); - fae->get_buffer(bytecode.ptrw(), bytecode.size()); - fae->close(); - memdelete(fae); - - } else { - bytecode = FileAccess::get_file_as_array(p_path); - } - - ERR_FAIL_COND_V(bytecode.size() == 0, ERR_PARSE_ERROR); - path = p_path; - - String basedir = path; - - if (basedir == "") { - basedir = get_path(); - } - - if (basedir != "") { - basedir = basedir.get_base_dir(); - } - - valid = false; - GDScriptParser parser; - Error err = parser.parse_bytecode(bytecode, basedir, get_path()); - if (err) { - _err_print_error("GDScript::load_byte_code", path.empty() ? "built-in" : (const char *)path.utf8().get_data(), parser.get_error_line(), ("Parse Error: " + parser.get_error()).utf8().get_data(), ERR_HANDLER_SCRIPT); - ERR_FAIL_V(ERR_PARSE_ERROR); - } - - GDScriptCompiler compiler; - err = compiler.compile(&parser, this); - - if (err) { - _err_print_error("GDScript::load_byte_code", path.empty() ? "built-in" : (const char *)path.utf8().get_data(), compiler.get_error_line(), ("Compile Error: " + compiler.get_error()).utf8().get_data(), ERR_HANDLER_SCRIPT); - ERR_FAIL_V(ERR_COMPILATION_FAILED); - } - - valid = true; - - for (Map<StringName, Ref<GDScript>>::Element *E = subclasses.front(); E; E = E->next()) { - _set_subclass_path(E->get(), path); - } - - _init_rpc_methods_properties(); - - return OK; + return ERR_COMPILATION_FAILED; } Error GDScript::load_source_code(const String &p_path) { @@ -1055,6 +1055,8 @@ GDScript::~GDScript() { memdelete(E->get()); } + GDScriptCache::remove_script(get_path()); + _save_orphaned_subclasses(); #ifdef DEBUG_ENABLED @@ -1153,6 +1155,32 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { } { + // Signals. + const GDScript *sl = sptr; + while (sl) { + const Map<StringName, Vector<StringName>>::Element *E = sl->_signals.find(p_name); + if (E) { + r_ret = Signal(this->owner, E->key()); + return true; //index found + } + sl = sl->_base; + } + } + + { + // Methods. + const GDScript *sl = sptr; + while (sl) { + const Map<StringName, GDScriptFunction *>::Element *E = sl->member_functions.find(p_name); + if (E) { + r_ret = Callable(this->owner, E->key()); + return true; //index found + } + sl = sl->_base; + } + } + + { const Map<StringName, GDScriptFunction *>::Element *E = sptr->member_functions.find(GDScriptLanguage::get_singleton()->strings._get); if (E) { Variant name = p_name; @@ -1296,38 +1324,6 @@ Variant GDScriptInstance::call(const StringName &p_method, const Variant **p_arg return Variant(); } -void GDScriptInstance::call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount) { - GDScript *sptr = script.ptr(); - Callable::CallError ce; - - while (sptr) { - Map<StringName, GDScriptFunction *>::Element *E = sptr->member_functions.find(p_method); - if (E) { - E->get()->call(this, p_args, p_argcount, ce); - } - sptr = sptr->_base; - } -} - -void GDScriptInstance::_ml_call_reversed(GDScript *sptr, const StringName &p_method, const Variant **p_args, int p_argcount) { - if (sptr->_base) { - _ml_call_reversed(sptr->_base, p_method, p_args, p_argcount); - } - - Callable::CallError ce; - - Map<StringName, GDScriptFunction *>::Element *E = sptr->member_functions.find(p_method); - if (E) { - E->get()->call(this, p_args, p_argcount, ce); - } -} - -void GDScriptInstance::call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount) { - if (script.ptr()) { - _ml_call_reversed(script.ptr(), p_method, p_args, p_argcount); - } -} - void GDScriptInstance::notification(int p_notification) { //notification is not virtual, it gets called at ALL levels just like in C. Variant value = p_notification; @@ -1827,6 +1823,7 @@ void GDScriptLanguage::frame() { /* EDITOR FUNCTIONS */ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { + // TODO: Add annotations here? static const char *_reserved_words[] = { // operators "and", @@ -1849,6 +1846,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { // functions "as", "assert", + "await", "breakpoint", "class", "class_name", @@ -1856,15 +1854,13 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { "is", "func", "preload", - "setget", "signal", - "tool", + "super", + "trait", "yield", // var "const", "enum", - "export", - "onready", "static", "var", // control flow @@ -1878,12 +1874,6 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { "return", "match", "while", - "remote", - "master", - "puppet", - "remotesync", - "mastersync", - "puppetsync", nullptr }; @@ -1914,10 +1904,11 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b String source = f->get_as_utf8_string(); GDScriptParser parser; - parser.parse(source, p_path.get_base_dir(), true, p_path, false, nullptr, true); + err = parser.parse(source, p_path, false); - if (parser.get_parse_tree() && parser.get_parse_tree()->type == GDScriptParser::Node::TYPE_CLASS) { - const GDScriptParser::ClassNode *c = static_cast<const GDScriptParser::ClassNode *>(parser.get_parse_tree()); + // TODO: Simplify this code by using the analyzer to get full inheritance. + if (err == OK) { + const GDScriptParser::ClassNode *c = parser.get_tree(); if (r_icon_path) { if (c->icon_path.empty() || c->icon_path.is_abs_path()) { *r_icon_path = c->icon_path; @@ -1931,15 +1922,15 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b GDScriptParser subparser; while (subclass) { if (subclass->extends_used) { - if (subclass->extends_file) { - if (subclass->extends_class.size() == 0) { - get_global_class_name(subclass->extends_file, r_base_type); + if (!subclass->extends_path.empty()) { + if (subclass->extends.size() == 0) { + get_global_class_name(subclass->extends_path, r_base_type); subclass = nullptr; break; } else { - Vector<StringName> extend_classes = subclass->extends_class; + Vector<StringName> extend_classes = subclass->extends; - FileAccessRef subfile = FileAccess::open(subclass->extends_file, FileAccess::READ); + FileAccessRef subfile = FileAccess::open(subclass->extends_path, FileAccess::READ); if (!subfile) { break; } @@ -1948,25 +1939,26 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b if (subsource.empty()) { break; } - String subpath = subclass->extends_file; + String subpath = subclass->extends_path; if (subpath.is_rel_path()) { subpath = path.get_base_dir().plus_file(subpath).simplify_path(); } - if (OK != subparser.parse(subsource, subpath.get_base_dir(), true, subpath, false, nullptr, true)) { + if (OK != subparser.parse(subsource, subpath, false)) { break; } path = subpath; - if (!subparser.get_parse_tree() || subparser.get_parse_tree()->type != GDScriptParser::Node::TYPE_CLASS) { - break; - } - subclass = static_cast<const GDScriptParser::ClassNode *>(subparser.get_parse_tree()); + subclass = subparser.get_tree(); while (extend_classes.size() > 0) { bool found = false; - for (int i = 0; i < subclass->subclasses.size(); i++) { - const GDScriptParser::ClassNode *inner_class = subclass->subclasses[i]; - if (inner_class->name == extend_classes[0]) { + for (int i = 0; i < subclass->members.size(); i++) { + if (subclass->members[i].type != GDScriptParser::ClassNode::Member::CLASS) { + continue; + } + + const GDScriptParser::ClassNode *inner_class = subclass->members[i].m_class; + if (inner_class->identifier->name == extend_classes[0]) { extend_classes.remove(0); found = true; subclass = inner_class; @@ -1979,8 +1971,8 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b } } } - } else if (subclass->extends_class.size() == 1) { - *r_base_type = subclass->extends_class[0]; + } else if (subclass->extends.size() == 1) { + *r_base_type = subclass->extends[0]; subclass = nullptr; } else { break; @@ -1991,183 +1983,12 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b } } } - return c->name; + return c->identifier != nullptr ? String(c->identifier->name) : String(); } return String(); } -#ifdef DEBUG_ENABLED -String GDScriptWarning::get_message() const { -#define CHECK_SYMBOLS(m_amount) ERR_FAIL_COND_V(symbols.size() < m_amount, String()); - - String msg; - - switch (code) { - case UNASSIGNED_VARIABLE_OP_ASSIGN: { - CHECK_SYMBOLS(1); - msg = "Using assignment with operation but the variable '" + symbols[0] + "' was not previously assigned a value."; - } break; - case UNASSIGNED_VARIABLE: { - CHECK_SYMBOLS(1); - msg = "The variable '" + symbols[0] + "' was used but never assigned a value."; - } break; - case UNUSED_VARIABLE: { - CHECK_SYMBOLS(1); - msg = "The local variable '" + symbols[0] + "' is declared but never used in the block. If this is intended, prefix it with an underscore: '_" + symbols[0] + "'"; - } break; - case SHADOWED_VARIABLE: { - CHECK_SYMBOLS(2); - msg = "The local variable '" + symbols[0] + "' is shadowing an already-defined variable at line " + symbols[1] + "."; - } break; - case UNUSED_CLASS_VARIABLE: { - CHECK_SYMBOLS(1); - msg = "The class variable '" + symbols[0] + "' is declared but never used in the script."; - } break; - case UNUSED_ARGUMENT: { - CHECK_SYMBOLS(2); - msg = "The argument '" + symbols[1] + "' is never used in the function '" + symbols[0] + "'. If this is intended, prefix it with an underscore: '_" + symbols[1] + "'"; - } break; - case UNREACHABLE_CODE: { - CHECK_SYMBOLS(1); - msg = "Unreachable code (statement after return) in function '" + symbols[0] + "()'."; - } break; - case STANDALONE_EXPRESSION: { - msg = "Standalone expression (the line has no effect)."; - } break; - case VOID_ASSIGNMENT: { - CHECK_SYMBOLS(1); - msg = "Assignment operation, but the function '" + symbols[0] + "()' returns void."; - } break; - case NARROWING_CONVERSION: { - msg = "Narrowing conversion (float is converted to int and loses precision)."; - } break; - case FUNCTION_MAY_YIELD: { - CHECK_SYMBOLS(1); - msg = "Assigned variable is typed but the function '" + symbols[0] + "()' may yield and return a GDScriptFunctionState instead."; - } break; - case VARIABLE_CONFLICTS_FUNCTION: { - CHECK_SYMBOLS(1); - msg = "Variable declaration of '" + symbols[0] + "' conflicts with a function of the same name."; - } break; - case FUNCTION_CONFLICTS_VARIABLE: { - CHECK_SYMBOLS(1); - msg = "Function declaration of '" + symbols[0] + "()' conflicts with a variable of the same name."; - } break; - case FUNCTION_CONFLICTS_CONSTANT: { - CHECK_SYMBOLS(1); - msg = "Function declaration of '" + symbols[0] + "()' conflicts with a constant of the same name."; - } break; - case INCOMPATIBLE_TERNARY: { - msg = "Values of the ternary conditional are not mutually compatible."; - } break; - case UNUSED_SIGNAL: { - CHECK_SYMBOLS(1); - msg = "The signal '" + symbols[0] + "' is declared but never emitted."; - } break; - case RETURN_VALUE_DISCARDED: { - CHECK_SYMBOLS(1); - msg = "The function '" + symbols[0] + "()' returns a value, but this value is never used."; - } break; - case PROPERTY_USED_AS_FUNCTION: { - CHECK_SYMBOLS(2); - msg = "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a property with the same name. Did you mean to access it?"; - } break; - case CONSTANT_USED_AS_FUNCTION: { - CHECK_SYMBOLS(2); - msg = "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a constant with the same name. Did you mean to access it?"; - } break; - case FUNCTION_USED_AS_PROPERTY: { - CHECK_SYMBOLS(2); - msg = "The property '" + symbols[0] + "' was not found in base '" + symbols[1] + "' but there's a method with the same name. Did you mean to call it?"; - } break; - case INTEGER_DIVISION: { - msg = "Integer division, decimal part will be discarded."; - } break; - case UNSAFE_PROPERTY_ACCESS: { - CHECK_SYMBOLS(2); - msg = "The property '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype)."; - } break; - case UNSAFE_METHOD_ACCESS: { - CHECK_SYMBOLS(2); - msg = "The method '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype)."; - } break; - case UNSAFE_CAST: { - CHECK_SYMBOLS(1); - msg = "The value is cast to '" + symbols[0] + "' but has an unknown type."; - } break; - case UNSAFE_CALL_ARGUMENT: { - CHECK_SYMBOLS(4); - msg = "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided"; - } break; - case DEPRECATED_KEYWORD: { - CHECK_SYMBOLS(2); - msg = "The '" + symbols[0] + "' keyword is deprecated and will be removed in a future release, please replace its uses by '" + symbols[1] + "'."; - } break; - case STANDALONE_TERNARY: { - msg = "Standalone ternary conditional operator: the return value is being discarded."; - } break; - case WARNING_MAX: - ERR_FAIL_V_MSG(String(), "Invalid GDScript warning code: " + get_name_from_code(code) + "."); - } - return msg + " [" + get_name() + "]"; - -#undef CHECK_SYMBOLS -} - -String GDScriptWarning::get_name() const { - return get_name_from_code(code); -} - -String GDScriptWarning::get_name_from_code(Code p_code) { - ERR_FAIL_COND_V(p_code < 0 || p_code >= WARNING_MAX, String()); - - static const char *names[] = { - "UNASSIGNED_VARIABLE", - "UNASSIGNED_VARIABLE_OP_ASSIGN", - "UNUSED_VARIABLE", - "SHADOWED_VARIABLE", - "UNUSED_CLASS_VARIABLE", - "UNUSED_ARGUMENT", - "UNREACHABLE_CODE", - "STANDALONE_EXPRESSION", - "VOID_ASSIGNMENT", - "NARROWING_CONVERSION", - "FUNCTION_MAY_YIELD", - "VARIABLE_CONFLICTS_FUNCTION", - "FUNCTION_CONFLICTS_VARIABLE", - "FUNCTION_CONFLICTS_CONSTANT", - "INCOMPATIBLE_TERNARY", - "UNUSED_SIGNAL", - "RETURN_VALUE_DISCARDED", - "PROPERTY_USED_AS_FUNCTION", - "CONSTANT_USED_AS_FUNCTION", - "FUNCTION_USED_AS_PROPERTY", - "INTEGER_DIVISION", - "UNSAFE_PROPERTY_ACCESS", - "UNSAFE_METHOD_ACCESS", - "UNSAFE_CAST", - "UNSAFE_CALL_ARGUMENT", - "DEPRECATED_KEYWORD", - "STANDALONE_TERNARY", - nullptr - }; - - return names[(int)p_code]; -} - -GDScriptWarning::Code GDScriptWarning::get_code_from_name(const String &p_name) { - for (int i = 0; i < WARNING_MAX; i++) { - if (get_name_from_code((Code)i) == p_name) { - return (Code)i; - } - } - - ERR_FAIL_V_MSG(WARNING_MAX, "Invalid GDScript warning name: " + p_name); -} - -#endif // DEBUG_ENABLED - GDScriptLanguage::GDScriptLanguage() { calls = 0; ERR_FAIL_COND(singleton); @@ -2206,7 +2027,7 @@ GDScriptLanguage::GDScriptLanguage() { GLOBAL_DEF("debug/gdscript/completion/autocomplete_setters_and_getters", false); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { String warning = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)i).to_lower(); - bool default_enabled = !warning.begins_with("unsafe_") && i != GDScriptWarning::UNUSED_CLASS_VARIABLE; + bool default_enabled = !warning.begins_with("unsafe_"); GLOBAL_DEF("debug/gdscript/warnings/" + warning, default_enabled); } #endif // DEBUG_ENABLED @@ -2244,36 +2065,28 @@ RES ResourceFormatLoaderGDScript::load(const String &p_path, const String &p_ori *r_error = ERR_FILE_CANT_OPEN; } - GDScript *script = memnew(GDScript); - - Ref<GDScript> scriptres(script); - - if (p_path.ends_with(".gde") || p_path.ends_with(".gdc")) { - script->set_script_path(p_original_path); // script needs this. - script->set_path(p_original_path); - Error err = script->load_byte_code(p_path); - ERR_FAIL_COND_V_MSG(err != OK, RES(), "Cannot load byte code from file '" + p_path + "'."); - - } else { - Error err = script->load_source_code(p_path); - ERR_FAIL_COND_V_MSG(err != OK, RES(), "Cannot load source code from file '" + p_path + "'."); + Error err; + Ref<GDScript> script = GDScriptCache::get_full_script(p_path, err); - script->set_script_path(p_original_path); // script needs this. - script->set_path(p_original_path); + // TODO: Reintroduce binary and encrypted scripts. - script->reload(); + if (script.is_null()) { + // Don't fail loading because of parsing error. + script.instance(); } + if (r_error) { *r_error = OK; } - return scriptres; + return script; } void ResourceFormatLoaderGDScript::get_recognized_extensions(List<String> *p_extensions) const { p_extensions->push_back("gd"); - p_extensions->push_back("gdc"); - p_extensions->push_back("gde"); + // TODO: Reintroduce binary and encrypted scripts. + // p_extensions->push_back("gdc"); + // p_extensions->push_back("gde"); } bool ResourceFormatLoaderGDScript::handles_type(const String &p_type) const { @@ -2282,7 +2095,8 @@ bool ResourceFormatLoaderGDScript::handles_type(const String &p_type) const { String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) const { String el = p_path.get_extension().to_lower(); - if (el == "gd" || el == "gdc" || el == "gde") { + // TODO: Reintroduce binary and encrypted scripts. + if (el == "gd" /*|| el == "gdc" || el == "gde"*/) { return "GDScript"; } return ""; @@ -2298,7 +2112,7 @@ void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<S } GDScriptParser parser; - if (OK != parser.parse(source, p_path.get_base_dir(), true, p_path, false, nullptr, true)) { + if (OK != parser.parse(source, p_path, false)) { return; } diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 9a5de2c293..9906b4014d 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -104,7 +104,8 @@ class GDScript : public Script { #endif Map<StringName, PropertyInfo> member_info; - GDScriptFunction *initializer; //direct pointer to _init , faster to locate + GDScriptFunction *implicit_initializer = nullptr; + GDScriptFunction *initializer = nullptr; //direct pointer to new , faster to locate int subclass_count; Set<Object *> instances; @@ -117,6 +118,7 @@ class GDScript : public Script { SelfList<GDScriptFunctionState>::List pending_func_states; + void _super_implicit_constructor(GDScript *p_script, GDScriptInstance *p_instance, Callable::CallError &r_error); GDScriptInstance *_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_isref, Callable::CallError &r_error); void _set_subclass_path(Ref<GDScript> &p_sc, const String &p_path); @@ -144,7 +146,6 @@ protected: void _get_property_list(List<PropertyInfo> *p_properties) const; Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; - //void call_multilevel(const StringName& p_method,const Variant** p_args,int p_argcount); static void _bind_methods(); @@ -256,8 +257,6 @@ class GDScriptInstance : public ScriptInstance { SelfList<GDScriptFunctionState>::List pending_func_states; - void _ml_call_reversed(GDScript *sptr, const StringName &p_method, const Variant **p_args, int p_argcount); - public: virtual Object *get_owner() { return owner; } @@ -269,8 +268,6 @@ public: virtual void get_method_list(List<MethodInfo> *p_list) const; virtual bool has_method(const StringName &p_method) const; virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error); - virtual void call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount); - virtual void call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount); Variant debug_get_member_by_index(int p_idx) const { return members[p_idx]; } @@ -301,52 +298,6 @@ public: ~GDScriptInstance(); }; -#ifdef DEBUG_ENABLED -struct GDScriptWarning { - enum Code { - UNASSIGNED_VARIABLE, // Variable used but never assigned - UNASSIGNED_VARIABLE_OP_ASSIGN, // Variable never assigned but used in an assignment operation (+=, *=, etc) - UNUSED_VARIABLE, // Local variable is declared but never used - SHADOWED_VARIABLE, // Variable name shadowed by other variable - UNUSED_CLASS_VARIABLE, // Class variable is declared but never used in the file - UNUSED_ARGUMENT, // Function argument is never used - UNREACHABLE_CODE, // Code after a return statement - STANDALONE_EXPRESSION, // Expression not assigned to a variable - VOID_ASSIGNMENT, // Function returns void but it's assigned to a variable - NARROWING_CONVERSION, // Float value into an integer slot, precision is lost - FUNCTION_MAY_YIELD, // Typed assign of function call that yields (it may return a function state) - VARIABLE_CONFLICTS_FUNCTION, // Variable has the same name of a function - FUNCTION_CONFLICTS_VARIABLE, // Function has the same name of a variable - FUNCTION_CONFLICTS_CONSTANT, // Function has the same name of a constant - INCOMPATIBLE_TERNARY, // Possible values of a ternary if are not mutually compatible - UNUSED_SIGNAL, // Signal is defined but never emitted - RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used - PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name - CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name - FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name - INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded - UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes) - UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes) - UNSAFE_CAST, // Cast used in an unknown type - UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument - DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced - STANDALONE_TERNARY, // Return value of ternary expression is discarded - WARNING_MAX, - }; - - Code code = WARNING_MAX; - Vector<String> symbols; - int line = -1; - - String get_name() const; - String get_message() const; - static String get_name_from_code(Code p_code); - static Code get_code_from_name(const String &p_name); - - GDScriptWarning() {} -}; -#endif // DEBUG_ENABLED - class GDScriptLanguage : public ScriptLanguage { friend class GDScriptFunctionState; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp new file mode 100644 index 0000000000..3253c8e945 --- /dev/null +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -0,0 +1,3256 @@ +/*************************************************************************/ +/* gdscript_analyzer.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#include "gdscript_analyzer.h" + +#include "core/class_db.h" +#include "core/hash_map.h" +#include "core/io/resource_loader.h" +#include "core/os/file_access.h" +#include "core/project_settings.h" +#include "core/script_language.h" +#include "gdscript.h" + +// TODO: Move this to a central location (maybe core?). +static HashMap<StringName, StringName> underscore_map; +static const char *underscore_classes[] = { + "ClassDB", + "Directory", + "Engine", + "File", + "Geometry", + "GodotSharp", + "JSON", + "Marshalls", + "Mutex", + "OS", + "ResourceLoader", + "ResourceSaver", + "Semaphore", + "Thread", + "VisualScriptEditor", + nullptr, +}; +static StringName get_real_class_name(const StringName &p_source) { + if (underscore_map.empty()) { + const char **class_name = underscore_classes; + while (*class_name != nullptr) { + underscore_map[*class_name] = String("_") + *class_name; + class_name++; + } + } + if (underscore_map.has(p_source)) { + return underscore_map[p_source]; + } + return p_source; +} + +void GDScriptAnalyzer::cleanup() { + underscore_map.clear(); +} + +static GDScriptParser::DataType make_callable_type(const MethodInfo &p_info) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::BUILTIN; + type.builtin_type = Variant::CALLABLE; + type.is_constant = true; + type.method_info = p_info; + return type; +} + +static GDScriptParser::DataType make_signal_type(const MethodInfo &p_info) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::BUILTIN; + type.builtin_type = Variant::SIGNAL; + type.is_constant = true; + type.method_info = p_info; + return type; +} + +static GDScriptParser::DataType make_native_meta_type(const StringName &p_class_name) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::NATIVE; + type.builtin_type = Variant::OBJECT; + type.is_constant = true; + type.native_type = p_class_name; + type.is_meta_type = true; + return type; +} + +static GDScriptParser::DataType make_native_enum_type(const StringName &p_native_class, const StringName &p_enum_name) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::ENUM; + type.builtin_type = Variant::OBJECT; + type.is_constant = true; + type.is_meta_type = true; + + List<StringName> enum_values; + StringName real_native_name = get_real_class_name(p_native_class); + ClassDB::get_enum_constants(real_native_name, p_enum_name, &enum_values); + + for (const List<StringName>::Element *E = enum_values.front(); E != nullptr; E = E->next()) { + type.enum_values[E->get()] = ClassDB::get_integer_constant(real_native_name, E->get()); + } + + return type; +} + +static GDScriptParser::DataType make_builtin_meta_type(Variant::Type p_type) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::BUILTIN; + type.builtin_type = p_type; + type.is_constant = true; + type.is_meta_type = true; + return type; +} + +Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, bool p_recursive) { + if (p_class->base_type.is_set()) { + // Already resolved + return OK; + } + + if (p_class == parser->head) { + if (p_class->identifier) { + p_class->fqcn = p_class->identifier->name; + } else { + p_class->fqcn = parser->script_path; + } + } else { + p_class->fqcn = p_class->outer->fqcn + "::" + String(p_class->identifier->name); + } + + GDScriptParser::DataType result; + + // Set datatype for class. + GDScriptParser::DataType class_type; + class_type.is_constant = true; + class_type.is_meta_type = true; + class_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + class_type.kind = GDScriptParser::DataType::CLASS; + class_type.class_type = p_class; + class_type.script_path = parser->script_path; + p_class->set_datatype(class_type); + + if (!p_class->extends_used) { + result.type_source = GDScriptParser::DataType::ANNOTATED_INFERRED; + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = "Reference"; + } else { + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + + GDScriptParser::DataType base; + + int extends_index = 0; + + if (!p_class->extends_path.empty()) { + Ref<GDScriptParserRef> parser = get_parser_for(p_class->extends_path); + if (parser.is_null()) { + push_error(vformat(R"(Could not resolve super class path "%s".)", p_class->extends_path), p_class); + return ERR_PARSE_ERROR; + } + + Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + if (err != OK) { + push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", p_class->extends_path), p_class); + return err; + } + + base = parser->get_parser()->head->get_datatype(); + } else { + if (p_class->extends.empty()) { + return ERR_PARSE_ERROR; + } + const StringName &name = p_class->extends[extends_index++]; + base.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + + if (ScriptServer::is_global_class(name)) { + String base_path = ScriptServer::get_global_class_path(name); + + if (base_path == parser->script_path) { + base = parser->head->get_datatype(); + } else { + Ref<GDScriptParserRef> parser = get_parser_for(base_path); + if (parser.is_null()) { + push_error(vformat(R"(Could not resolve super class "%s".)", name), p_class); + return ERR_PARSE_ERROR; + } + + Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + if (err != OK) { + push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), p_class); + return err; + } + } + } else if (ProjectSettings::get_singleton()->has_autoload(name) && ProjectSettings::get_singleton()->get_autoload(name).is_singleton) { + const ProjectSettings::AutoloadInfo &info = ProjectSettings::get_singleton()->get_autoload(name); + if (info.path.get_extension().to_lower() != ".gd") { + push_error(vformat(R"(Singleton %s is not a GDScript.)", info.name), p_class); + return ERR_PARSE_ERROR; + } + + Ref<GDScriptParserRef> parser = get_parser_for(info.path); + if (parser.is_null()) { + push_error(vformat(R"(Could not parse singleton from "%s".)", info.path), p_class); + return ERR_PARSE_ERROR; + } + + Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + if (err != OK) { + push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), p_class); + return err; + } + } else if (class_exists(name) && ClassDB::can_instance(get_real_class_name(name))) { + base.kind = GDScriptParser::DataType::NATIVE; + base.native_type = name; + } else { + // Look for other classes in script. + GDScriptParser::ClassNode *look_class = p_class; + bool found = false; + while (look_class != nullptr) { + if (look_class->identifier && look_class->identifier->name == name) { + if (!look_class->get_datatype().is_set()) { + Error err = resolve_inheritance(look_class, false); + if (err) { + return err; + } + } + base = look_class->get_datatype(); + found = true; + break; + } + if (look_class->members_indices.has(name) && look_class->get_member(name).type == GDScriptParser::ClassNode::Member::CLASS) { + GDScriptParser::ClassNode::Member member = look_class->get_member(name); + if (!member.m_class->get_datatype().is_set()) { + Error err = resolve_inheritance(member.m_class, false); + if (err) { + return err; + } + } + base = member.m_class->get_datatype(); + found = true; + break; + } + look_class = look_class->outer; + } + + if (!found) { + push_error(vformat(R"(Could not find base class "%s".)", name), p_class); + return ERR_PARSE_ERROR; + } + } + } + + for (int index = extends_index; index < p_class->extends.size(); index++) { + if (base.kind != GDScriptParser::DataType::CLASS) { + push_error(R"(Super type "%s" is not a GDScript. Cannot get nested types.)", p_class); + return ERR_PARSE_ERROR; + } + + // TODO: Extends could use identifier nodes. That way errors can be pointed out properly and it can be used here. + GDScriptParser::IdentifierNode *id = parser->alloc_node<GDScriptParser::IdentifierNode>(); + id->name = p_class->extends[index]; + + reduce_identifier_from_base(id, &base); + + GDScriptParser::DataType id_type = id->get_datatype(); + if (!id_type.is_set()) { + push_error(vformat(R"(Could not find type "%s" under base "%s".)", id->name, base.to_string()), p_class); + } + + base = id_type; + } + + result = base; + } + + if (!result.is_set()) { + // TODO: More specific error messages. + push_error(vformat(R"(Could not resolve inheritance for class "%s".)", p_class->identifier == nullptr ? "<main>" : p_class->identifier->name), p_class); + return ERR_PARSE_ERROR; + } + + p_class->base_type = result; + class_type.native_type = result.native_type; + p_class->set_datatype(class_type); + + if (p_recursive) { + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type == GDScriptParser::ClassNode::Member::CLASS) { + resolve_inheritance(p_class->members[i].m_class, true); + } + } + } + + return OK; +} + +GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::TypeNode *p_type) { + GDScriptParser::DataType result; + + if (p_type == nullptr) { + result.kind = GDScriptParser::DataType::VARIANT; + return result; + } + + result.type_source = result.ANNOTATED_EXPLICIT; + result.builtin_type = Variant::OBJECT; + + if (p_type->type_chain.empty()) { + // void. + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::NIL; + p_type->set_datatype(result); + return result; + } + + StringName first = p_type->type_chain[0]->name; + + if (first == "Variant") { + result.kind = GDScriptParser::DataType::VARIANT; + if (p_type->type_chain.size() > 1) { + push_error(R"("Variant" type don't contain nested types.)", p_type->type_chain[1]); + return GDScriptParser::DataType(); + } + return result; + } + + if (first == "Object") { + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = "Object"; + if (p_type->type_chain.size() > 1) { + push_error(R"("Object" type don't contain nested types.)", p_type->type_chain[1]); + return GDScriptParser::DataType(); + } + return result; + } + + if (GDScriptParser::get_builtin_type(first) < Variant::VARIANT_MAX) { + // Built-in types. + if (p_type->type_chain.size() > 1) { + push_error(R"(Built-in types don't contain nested types.)", p_type->type_chain[1]); + return GDScriptParser::DataType(); + } + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = GDScriptParser::get_builtin_type(first); + } else if (class_exists(first)) { + // Native engine classes. + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = first; + } else if (ScriptServer::is_global_class(first)) { + if (parser->script_path == ScriptServer::get_global_class_path(first)) { + result = parser->head->get_datatype(); + } else { + Ref<GDScriptParserRef> ref = get_parser_for(ScriptServer::get_global_class_path(first)); + if (ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED) != OK) { + push_error(vformat(R"(Could not parse global class "%s" from "%s".)", first, ScriptServer::get_global_class_path(first)), p_type); + return GDScriptParser::DataType(); + } + result = ref->get_parser()->head->get_datatype(); + } + } else if (ProjectSettings::get_singleton()->has_autoload(first) && ProjectSettings::get_singleton()->get_autoload(first).is_singleton) { + const ProjectSettings::AutoloadInfo &autoload = ProjectSettings::get_singleton()->get_autoload(first); + Ref<GDScriptParserRef> ref = get_parser_for(autoload.path); + if (ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED) != OK) { + push_error(vformat(R"(Could not parse singleton "%s" from "%s".)", first, autoload.path), p_type); + return GDScriptParser::DataType(); + } + result = ref->get_parser()->head->get_datatype(); + } else if (ClassDB::has_enum(get_real_class_name(parser->current_class->base_type.native_type), first)) { + // Native enum in current class. + result = make_native_enum_type(parser->current_class->base_type.native_type, first); + } else { + // Classes in current scope. + GDScriptParser::ClassNode *script_class = parser->current_class; + bool found = false; + while (!found && script_class != nullptr) { + if (script_class->identifier && script_class->identifier->name == first) { + result = script_class->get_datatype(); + found = true; + break; + } + if (script_class->members_indices.has(first)) { + GDScriptParser::ClassNode::Member member = script_class->members[script_class->members_indices[first]]; + switch (member.type) { + case GDScriptParser::ClassNode::Member::CLASS: + result = member.m_class->get_datatype(); + found = true; + break; + case GDScriptParser::ClassNode::Member::ENUM: + result = member.m_enum->get_datatype(); + found = true; + break; + case GDScriptParser::ClassNode::Member::CONSTANT: + if (member.constant->get_datatype().is_meta_type) { + result = member.constant->get_datatype(); + found = true; + break; + } + [[fallthrough]]; + default: + push_error(vformat(R"("%s" is a %s but does not contain a type.)", first, member.get_type_name()), p_type); + return GDScriptParser::DataType(); + } + } + script_class = script_class->outer; + } + } + if (!result.is_set()) { + push_error(vformat(R"("%s" was not found in the current scope.)", first), p_type); + result.kind = GDScriptParser::DataType::VARIANT; // Leave Variant anyway so future type check don't use an unresolved type. + return result; + } + + if (p_type->type_chain.size() > 1) { + if (result.kind == GDScriptParser::DataType::CLASS) { + for (int i = 1; i < p_type->type_chain.size(); i++) { + GDScriptParser::DataType base = result; + reduce_identifier_from_base(p_type->type_chain[i], &base); + result = p_type->type_chain[i]->get_datatype(); + if (!result.is_set()) { + push_error(vformat(R"(Could not find type "%s" under base "%s".)", p_type->type_chain[i]->name, base.to_string()), p_type->type_chain[1]); + result.kind = GDScriptParser::DataType::VARIANT; // Leave Variant anyway so future type check don't use an unresolved type. + return result; + } else if (!result.is_meta_type) { + push_error(vformat(R"(Member "%s" under base "%s" is not a valid type.)", p_type->type_chain[i]->name, base.to_string()), p_type->type_chain[1]); + result.kind = GDScriptParser::DataType::VARIANT; // Leave Variant anyway so future type check don't use an unresolved type. + return result; + } + } + } else if (result.kind == GDScriptParser::DataType::NATIVE) { + // Only enums allowed for native. + if (ClassDB::has_enum(get_real_class_name(result.native_type), p_type->type_chain[1]->name)) { + if (p_type->type_chain.size() > 2) { + push_error(R"(Enums cannot contain nested types.)", p_type->type_chain[2]); + } else { + result = make_native_enum_type(result.native_type, p_type->type_chain[1]->name); + } + } + } else { + push_error(vformat(R"(Could not find nested type "%s" under base "%s".)", p_type->type_chain[1]->name, result.to_string()), p_type->type_chain[1]); + result.kind = GDScriptParser::DataType::VARIANT; // Leave Variant anyway so future type check don't use an unresolved type. + return result; + } + } + + p_type->set_datatype(result); + return result; +} + +void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_class) { + if (p_class->resolved_interface) { + return; + } + p_class->resolved_interface = true; + + GDScriptParser::ClassNode *previous_class = parser->current_class; + parser->current_class = p_class; + + for (int i = 0; i < p_class->members.size(); i++) { + GDScriptParser::ClassNode::Member member = p_class->members[i]; + + switch (member.type) { + case GDScriptParser::ClassNode::Member::VARIABLE: { + GDScriptParser::DataType datatype; + datatype.kind = GDScriptParser::DataType::VARIANT; + datatype.type_source = GDScriptParser::DataType::UNDETECTED; + + if (member.variable->initializer != nullptr) { + member.variable->set_datatype(datatype); // Allow recursive usage. + reduce_expression(member.variable->initializer); + datatype = member.variable->initializer->get_datatype(); + } + + if (member.variable->datatype_specifier != nullptr) { + datatype = resolve_datatype(member.variable->datatype_specifier); + datatype.is_meta_type = false; + + if (member.variable->initializer != nullptr) { + if (!is_type_compatible(datatype, member.variable->initializer->get_datatype(), true)) { + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(member.variable->initializer->get_datatype(), datatype, true)) { + push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", member.variable->initializer->get_datatype().to_string(), datatype.to_string()), member.variable->initializer); + } else { + // TODO: Add warning. + mark_node_unsafe(member.variable->initializer); + } + } else if (datatype.builtin_type == Variant::INT && member.variable->initializer->get_datatype().builtin_type == Variant::FLOAT) { +#ifdef DEBUG_ENABLED + parser->push_warning(member.variable->initializer, GDScriptWarning::NARROWING_CONVERSION); +#endif + } + if (member.variable->initializer->get_datatype().is_variant()) { + // TODO: Warn unsafe assign. + mark_node_unsafe(member.variable->initializer); + } + } + } else if (member.variable->infer_datatype) { + if (member.variable->initializer == nullptr) { + push_error(vformat(R"(Cannot infer the type of "%s" variable because there's no default value.)", member.variable->identifier->name), member.variable->identifier); + } else if (!datatype.is_set() || datatype.has_no_type()) { + push_error(vformat(R"(Cannot infer the type of "%s" variable because the initial value doesn't have a set type.)", member.variable->identifier->name), member.variable->initializer); + } else if (datatype.is_variant()) { + push_error(vformat(R"(Cannot infer the type of "%s" variable because the initial value is Variant. Use explicit "Variant" type if this is intended.)", member.variable->identifier->name), member.variable->initializer); + } else if (datatype.builtin_type == Variant::NIL) { + push_error(vformat(R"(Cannot infer the type of "%s" variable because the initial value is "null".)", member.variable->identifier->name), member.variable->initializer); + } + } + + datatype.is_constant = false; + member.variable->set_datatype(datatype); + if (!datatype.has_no_type()) { + // TODO: Move this out into a routine specific to validate annotations. + if (member.variable->export_info.hint == PROPERTY_HINT_TYPE_STRING) { + // @export annotation. + switch (datatype.kind) { + case GDScriptParser::DataType::BUILTIN: + member.variable->export_info.hint_string = Variant::get_type_name(datatype.builtin_type); + break; + case GDScriptParser::DataType::NATIVE: + if (ClassDB::is_parent_class(get_real_class_name(datatype.native_type), "Resource")) { + member.variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; + member.variable->export_info.hint_string = get_real_class_name(datatype.native_type); + } else { + push_error(R"(Export type can only be built-in or a resource.)", member.variable); + } + break; + default: + // TODO: Allow custom user resources. + push_error(R"(Export type can only be built-in or a resource.)", member.variable); + break; + } + } + } + } break; + case GDScriptParser::ClassNode::Member::CONSTANT: { + reduce_expression(member.constant->initializer); + + GDScriptParser::DataType datatype = member.constant->get_datatype(); + if (member.constant->initializer) { + if (!member.constant->initializer->is_constant) { + push_error(R"(Initializer for a constant must be a constant expression.)", member.constant->initializer); + } + + if (member.constant->datatype_specifier != nullptr) { + datatype = resolve_datatype(member.constant->datatype_specifier); + datatype.is_meta_type = false; + + if (!is_type_compatible(datatype, member.constant->initializer->get_datatype(), true)) { + push_error(vformat(R"(Value of type "%s" cannot be initialized to constant of type "%s".)", member.constant->initializer->get_datatype().to_string(), datatype.to_string()), member.constant->initializer); + } else if (datatype.builtin_type == Variant::INT && member.constant->initializer->get_datatype().builtin_type == Variant::FLOAT) { +#ifdef DEBUG_ENABLED + parser->push_warning(member.constant->initializer, GDScriptWarning::NARROWING_CONVERSION); +#endif + } + } + } + datatype.is_constant = true; + + member.constant->set_datatype(datatype); + } break; + case GDScriptParser::ClassNode::Member::SIGNAL: { + for (int j = 0; j < member.signal->parameters.size(); j++) { + GDScriptParser::DataType signal_type = resolve_datatype(member.signal->parameters[j]->datatype_specifier); + signal_type.is_meta_type = false; + member.signal->parameters[j]->set_datatype(signal_type); + } + // TODO: Make MethodInfo from signal. + GDScriptParser::DataType signal_type; + signal_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + signal_type.kind = GDScriptParser::DataType::BUILTIN; + signal_type.builtin_type = Variant::SIGNAL; + + member.signal->set_datatype(signal_type); + } break; + case GDScriptParser::ClassNode::Member::ENUM: { + GDScriptParser::DataType enum_type; + enum_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + enum_type.kind = GDScriptParser::DataType::ENUM; + enum_type.builtin_type = Variant::DICTIONARY; + enum_type.enum_type = member.m_enum->identifier->name; + enum_type.native_type = p_class->fqcn + "." + member.m_enum->identifier->name; + enum_type.is_meta_type = true; + enum_type.is_constant = true; + + // Enums can't be nested, so we can safely override this. + current_enum = member.m_enum; + + for (int j = 0; j < member.m_enum->values.size(); j++) { + GDScriptParser::EnumNode::Value &element = member.m_enum->values.write[j]; + + if (element.custom_value) { + reduce_expression(element.custom_value); + if (!element.custom_value->is_constant) { + push_error(R"(Enum values must be constant.)", element.custom_value); + } else if (element.custom_value->reduced_value.get_type() != Variant::INT) { + push_error(R"(Enum values must be integers.)", element.custom_value); + } else { + element.value = element.custom_value->reduced_value; + element.resolved = true; + } + } else { + if (element.index > 0) { + element.value = element.parent_enum->values[element.index - 1].value + 1; + } else { + element.value = 0; + } + element.resolved = true; + } + + enum_type.enum_values[element.identifier->name] = element.value; + } + + current_enum = nullptr; + + member.m_enum->set_datatype(enum_type); + } break; + case GDScriptParser::ClassNode::Member::FUNCTION: + resolve_function_signature(member.function); + break; + case GDScriptParser::ClassNode::Member::ENUM_VALUE: { + if (member.enum_value.custom_value) { + current_enum = member.enum_value.parent_enum; + reduce_expression(member.enum_value.custom_value); + current_enum = nullptr; + + if (!member.enum_value.custom_value->is_constant) { + push_error(R"(Enum values must be constant.)", member.enum_value.custom_value); + } else if (member.enum_value.custom_value->reduced_value.get_type() != Variant::INT) { + push_error(R"(Enum values must be integers.)", member.enum_value.custom_value); + } else { + member.enum_value.value = member.enum_value.custom_value->reduced_value; + member.enum_value.resolved = true; + } + } else { + if (member.enum_value.index > 0) { + member.enum_value.value = member.enum_value.parent_enum->values[member.enum_value.index - 1].value + 1; + } else { + member.enum_value.value = 0; + } + member.enum_value.resolved = true; + } + // Also update the original references. + member.enum_value.parent_enum->values.write[member.enum_value.index] = member.enum_value; + p_class->members.write[i].enum_value = member.enum_value; + } break; + case GDScriptParser::ClassNode::Member::CLASS: + break; // Done later. + case GDScriptParser::ClassNode::Member::UNDEFINED: + ERR_PRINT("Trying to resolve undefined member."); + break; + } + } + + // Recurse nested classes. + for (int i = 0; i < p_class->members.size(); i++) { + GDScriptParser::ClassNode::Member member = p_class->members[i]; + if (member.type != GDScriptParser::ClassNode::Member::CLASS) { + continue; + } + + resolve_class_interface(member.m_class); + } + + parser->current_class = previous_class; +} + +void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class) { + if (p_class->resolved_body) { + return; + } + p_class->resolved_body = true; + + GDScriptParser::ClassNode *previous_class = parser->current_class; + parser->current_class = p_class; + + // Do functions now. + for (int i = 0; i < p_class->members.size(); i++) { + GDScriptParser::ClassNode::Member member = p_class->members[i]; + if (member.type != GDScriptParser::ClassNode::Member::FUNCTION) { + continue; + } + + resolve_function_body(member.function); + } + + parser->current_class = previous_class; + + // Recurse nested classes. + for (int i = 0; i < p_class->members.size(); i++) { + GDScriptParser::ClassNode::Member member = p_class->members[i]; + if (member.type != GDScriptParser::ClassNode::Member::CLASS) { + continue; + } + + resolve_class_body(member.m_class); + } + + // Check unused variables. + for (int i = 0; i < p_class->members.size(); i++) { + GDScriptParser::ClassNode::Member member = p_class->members[i]; + if (member.type != GDScriptParser::ClassNode::Member::VARIABLE) { + continue; + } +#ifdef DEBUG_ENABLED + if (member.variable->usages == 0 && String(member.variable->identifier->name).begins_with("_")) { + parser->push_warning(member.variable->identifier, GDScriptWarning::UNUSED_PRIVATE_CLASS_VARIABLE, member.variable->identifier->name); + } +#endif + } +} + +void GDScriptAnalyzer::resolve_node(GDScriptParser::Node *p_node) { + ERR_FAIL_COND_MSG(p_node == nullptr, "Trying to resolve type of a null node."); + + switch (p_node->type) { + case GDScriptParser::Node::NONE: + break; // Unreachable. + case GDScriptParser::Node::CLASS: + resolve_class_interface(static_cast<GDScriptParser::ClassNode *>(p_node)); + resolve_class_body(static_cast<GDScriptParser::ClassNode *>(p_node)); + break; + case GDScriptParser::Node::CONSTANT: + resolve_constant(static_cast<GDScriptParser::ConstantNode *>(p_node)); + break; + case GDScriptParser::Node::FOR: + resolve_for(static_cast<GDScriptParser::ForNode *>(p_node)); + break; + case GDScriptParser::Node::FUNCTION: + resolve_function_signature(static_cast<GDScriptParser::FunctionNode *>(p_node)); + resolve_function_body(static_cast<GDScriptParser::FunctionNode *>(p_node)); + break; + case GDScriptParser::Node::IF: + resolve_if(static_cast<GDScriptParser::IfNode *>(p_node)); + break; + case GDScriptParser::Node::SUITE: + resolve_suite(static_cast<GDScriptParser::SuiteNode *>(p_node)); + break; + case GDScriptParser::Node::VARIABLE: + resolve_variable(static_cast<GDScriptParser::VariableNode *>(p_node)); + break; + case GDScriptParser::Node::WHILE: + resolve_while(static_cast<GDScriptParser::WhileNode *>(p_node)); + break; + case GDScriptParser::Node::ANNOTATION: + resolve_annotation(static_cast<GDScriptParser::AnnotationNode *>(p_node)); + break; + case GDScriptParser::Node::ASSERT: + resolve_assert(static_cast<GDScriptParser::AssertNode *>(p_node)); + break; + case GDScriptParser::Node::MATCH: + resolve_match(static_cast<GDScriptParser::MatchNode *>(p_node)); + break; + case GDScriptParser::Node::MATCH_BRANCH: + resolve_match_branch(static_cast<GDScriptParser::MatchBranchNode *>(p_node), nullptr); + break; + case GDScriptParser::Node::PARAMETER: + resolve_pararameter(static_cast<GDScriptParser::ParameterNode *>(p_node)); + break; + case GDScriptParser::Node::PATTERN: + resolve_match_pattern(static_cast<GDScriptParser::PatternNode *>(p_node), nullptr); + break; + case GDScriptParser::Node::RETURN: + resolve_return(static_cast<GDScriptParser::ReturnNode *>(p_node)); + break; + case GDScriptParser::Node::TYPE: + resolve_datatype(static_cast<GDScriptParser::TypeNode *>(p_node)); + break; + // Resolving expression is the same as reducing them. + case GDScriptParser::Node::ARRAY: + case GDScriptParser::Node::ASSIGNMENT: + case GDScriptParser::Node::AWAIT: + case GDScriptParser::Node::BINARY_OPERATOR: + case GDScriptParser::Node::CALL: + case GDScriptParser::Node::CAST: + case GDScriptParser::Node::DICTIONARY: + case GDScriptParser::Node::GET_NODE: + case GDScriptParser::Node::IDENTIFIER: + case GDScriptParser::Node::LITERAL: + case GDScriptParser::Node::PRELOAD: + case GDScriptParser::Node::SELF: + case GDScriptParser::Node::SUBSCRIPT: + case GDScriptParser::Node::TERNARY_OPERATOR: + case GDScriptParser::Node::UNARY_OPERATOR: + reduce_expression(static_cast<GDScriptParser::ExpressionNode *>(p_node)); + break; + case GDScriptParser::Node::BREAK: + case GDScriptParser::Node::BREAKPOINT: + case GDScriptParser::Node::CONTINUE: + case GDScriptParser::Node::ENUM: + case GDScriptParser::Node::PASS: + case GDScriptParser::Node::SIGNAL: + // Nothing to do. + break; + } +} + +void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_annotation) { + // TODO: Add second validation function for annotations, so they can use checked types. +} + +void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *p_function) { + if (p_function->resolved_signature) { + return; + } + p_function->resolved_signature = true; + + GDScriptParser::FunctionNode *previous_function = parser->current_function; + parser->current_function = p_function; + + for (int i = 0; i < p_function->parameters.size(); i++) { + resolve_pararameter(p_function->parameters[i]); +#ifdef DEBUG_ENABLED + if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_")) { + parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, p_function->identifier->name, p_function->parameters[i]->identifier->name); + } + is_shadowing(p_function->parameters[i]->identifier, "function parameter"); +#endif + } + + if (p_function->identifier->name == "_init") { + // Constructor. + GDScriptParser::DataType return_type = parser->current_class->get_datatype(); + return_type.is_meta_type = false; + p_function->set_datatype(return_type); + if (p_function->return_type) { + push_error("Constructor cannot have an explicit return type.", p_function->return_type); + } + } else { + GDScriptParser::DataType return_type = resolve_datatype(p_function->return_type); + p_function->set_datatype(return_type); + } + + parser->current_function = previous_function; +} + +void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_function) { + if (p_function->resolved_body) { + return; + } + p_function->resolved_body = true; + + GDScriptParser::FunctionNode *previous_function = parser->current_function; + parser->current_function = p_function; + + resolve_suite(p_function->body); + + GDScriptParser::DataType return_type = p_function->body->get_datatype(); + + if (p_function->get_datatype().has_no_type() && return_type.is_set()) { + // Use the suite inferred type if return isn't explicitly set. + return_type.type_source = GDScriptParser::DataType::INFERRED; + p_function->set_datatype(p_function->body->get_datatype()); + } else if (p_function->get_datatype().is_hard_type() && (p_function->get_datatype().kind != GDScriptParser::DataType::BUILTIN || p_function->get_datatype().builtin_type != Variant::NIL)) { + if (!p_function->body->has_return && p_function->identifier->name != GDScriptLanguage::get_singleton()->strings._init) { + push_error(R"(Not all code paths return a value.)", p_function); + } + } + + parser->current_function = previous_function; +} + +void GDScriptAnalyzer::decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement) { + if (p_statement == nullptr) { + return; + } + switch (p_statement->type) { + case GDScriptParser::Node::IF: + case GDScriptParser::Node::FOR: + case GDScriptParser::Node::MATCH: + case GDScriptParser::Node::PATTERN: + case GDScriptParser::Node::RETURN: + case GDScriptParser::Node::WHILE: + // Use return or nested suite type as this suite type. + if (p_suite->get_datatype().is_set() && (p_suite->get_datatype() != p_statement->get_datatype())) { + // Mixed types. + // TODO: This could use the common supertype instead. + p_suite->datatype.kind = GDScriptParser::DataType::VARIANT; + p_suite->datatype.type_source = GDScriptParser::DataType::UNDETECTED; + } else { + p_suite->set_datatype(p_statement->get_datatype()); + } + break; + default: + break; + } +} + +void GDScriptAnalyzer::resolve_suite(GDScriptParser::SuiteNode *p_suite) { + for (int i = 0; i < p_suite->statements.size(); i++) { + GDScriptParser::Node *stmt = p_suite->statements[i]; + resolve_node(stmt); + decide_suite_type(p_suite, stmt); + } +} + +void GDScriptAnalyzer::resolve_if(GDScriptParser::IfNode *p_if) { + reduce_expression(p_if->condition); + + resolve_suite(p_if->true_block); + p_if->set_datatype(p_if->true_block->get_datatype()); + + if (p_if->false_block != nullptr) { + resolve_suite(p_if->false_block); + decide_suite_type(p_if, p_if->false_block); + } +} + +void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { + bool list_resolved = false; + + // Optimize constant range() call to not allocate an array. + // Use int, Vector2, Vector3 instead, which also can be used as range iterators. + if (p_for->list && p_for->list->type == GDScriptParser::Node::CALL) { + GDScriptParser::CallNode *call = static_cast<GDScriptParser::CallNode *>(p_for->list); + GDScriptParser::Node::Type callee_type = call->get_callee_type(); + if (callee_type == GDScriptParser::Node::IDENTIFIER) { + GDScriptParser::IdentifierNode *callee = static_cast<GDScriptParser::IdentifierNode *>(call->callee); + if (callee->name == "range") { + list_resolved = true; + if (call->arguments.size() < 1) { + push_error(R"*(Invalid call for "range()" function. Expected at least 1 argument, none given.)*", call->callee); + } else if (call->arguments.size() > 3) { + push_error(vformat(R"*(Invalid call for "range()" function. Expected at most 3 arguments, %d given.)*", call->arguments.size()), call->callee); + } else { + // Now we can optimize it. + bool all_is_constant = true; + Vector<Variant> args; + args.resize(call->arguments.size()); + for (int i = 0; i < call->arguments.size(); i++) { + reduce_expression(call->arguments[i]); + + if (!call->arguments[i]->is_constant) { + all_is_constant = false; + } else { + args.write[i] = call->arguments[i]->reduced_value; + } + + GDScriptParser::DataType arg_type = call->arguments[i]->get_datatype(); + if (arg_type.kind != GDScriptParser::DataType::BUILTIN) { + all_is_constant = false; + push_error(vformat(R"*(Invalid argument for "range()" call. Argument %d should be int or float but "%s" was given.)*", i + 1, arg_type.to_string()), call->arguments[i]); + } else if (arg_type.builtin_type != Variant::INT && arg_type.builtin_type != Variant::FLOAT) { + all_is_constant = false; + push_error(vformat(R"*(Invalid argument for "range()" call. Argument %d should be int or float but "%s" was given.)*", i + 1, arg_type.to_string()), call->arguments[i]); + } + } + + Variant reduced; + + if (all_is_constant) { + switch (args.size()) { + case 1: + reduced = args[0]; + break; + case 2: + reduced = Vector2i(args[0], args[1]); + break; + case 3: + reduced = Vector3i(args[0], args[1], args[2]); + break; + } + p_for->list->is_constant = true; + p_for->list->reduced_value = reduced; + } + } + + GDScriptParser::DataType list_type; + list_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + list_type.kind = GDScriptParser::DataType::BUILTIN; + list_type.builtin_type = Variant::ARRAY; + p_for->list->set_datatype(list_type); + } + } + } + + if (!list_resolved) { + resolve_node(p_for->list); + } + + // TODO: If list is a typed array, the variable should be an element. + // Also applicable for constant range() (so variable is int or float). + + resolve_suite(p_for->loop); + p_for->set_datatype(p_for->loop->get_datatype()); +#ifdef DEBUG_ENABLED + if (p_for->variable) { + is_shadowing(p_for->variable, R"("for" iterator variable)"); + } +#endif +} + +void GDScriptAnalyzer::resolve_while(GDScriptParser::WhileNode *p_while) { + resolve_node(p_while->condition); + + resolve_suite(p_while->loop); + p_while->set_datatype(p_while->loop->get_datatype()); +} + +void GDScriptAnalyzer::resolve_variable(GDScriptParser::VariableNode *p_variable) { + GDScriptParser::DataType type; + type.kind = GDScriptParser::DataType::VARIANT; // By default. + + if (p_variable->initializer != nullptr) { + reduce_expression(p_variable->initializer); + type = p_variable->initializer->get_datatype(); + + if (p_variable->infer_datatype) { + type.type_source = GDScriptParser::DataType::ANNOTATED_INFERRED; + + if (type.has_no_type()) { + push_error(vformat(R"(Could not infer the type of the variable "%s" because the initial value does not have a set type.)", p_variable->identifier->name), p_variable->initializer); + } else if (type.is_variant()) { + push_error(vformat(R"(Could not infer the type of the variable "%s" because the initial value is a variant. Use explicit "Variant" type if this is intended.)", p_variable->identifier->name), p_variable->initializer); + } else if (type.kind == GDScriptParser::DataType::BUILTIN && type.builtin_type == Variant::NIL) { + push_error(vformat(R"(Could not infer the type of the variable "%s" because the initial value is "null".)", p_variable->identifier->name), p_variable->initializer); + } + } else { + type.type_source = GDScriptParser::DataType::INFERRED; + } +#ifdef DEBUG_ENABLED + if (p_variable->initializer->type == GDScriptParser::Node::CALL && type.kind == GDScriptParser::DataType::BUILTIN && type.builtin_type == Variant::NIL) { + parser->push_warning(p_variable->initializer, GDScriptWarning::VOID_ASSIGNMENT, static_cast<GDScriptParser::CallNode *>(p_variable->initializer)->function_name); + } +#endif + } + + if (p_variable->datatype_specifier != nullptr) { + type = resolve_datatype(p_variable->datatype_specifier); + type.is_meta_type = false; + + if (p_variable->initializer != nullptr) { + if (!is_type_compatible(type, p_variable->initializer->get_datatype(), true)) { + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(p_variable->initializer->get_datatype(), type, true)) { + push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", p_variable->initializer->get_datatype().to_string(), type.to_string()), p_variable->initializer); + } else { + // TODO: Add warning. + mark_node_unsafe(p_variable->initializer); + } +#ifdef DEBUG_ENABLED + } else if (type.builtin_type == Variant::INT && p_variable->initializer->get_datatype().builtin_type == Variant::FLOAT) { + parser->push_warning(p_variable->initializer, GDScriptWarning::NARROWING_CONVERSION); +#endif + } + if (p_variable->initializer->get_datatype().is_variant()) { + // TODO: Warn unsafe assign. + mark_node_unsafe(p_variable->initializer); + } + } + } else if (p_variable->infer_datatype) { + if (type.has_no_type()) { + push_error(vformat(R"(Cannot infer the type of variable "%s" because the initial value doesn't have a set type.)", p_variable->identifier->name), p_variable->identifier); + } + type.type_source = GDScriptParser::DataType::ANNOTATED_INFERRED; + } + + type.is_constant = false; + p_variable->set_datatype(type); + +#ifdef DEBUG_ENABLED + if (p_variable->usages == 0 && !String(p_variable->identifier->name).begins_with("_")) { + parser->push_warning(p_variable, GDScriptWarning::UNUSED_VARIABLE, p_variable->identifier->name); + } else if (p_variable->assignments == 0) { + parser->push_warning(p_variable, GDScriptWarning::UNASSIGNED_VARIABLE, p_variable->identifier->name); + } + + is_shadowing(p_variable->identifier, "variable"); +#endif +} + +void GDScriptAnalyzer::resolve_constant(GDScriptParser::ConstantNode *p_constant) { + GDScriptParser::DataType type; + + reduce_expression(p_constant->initializer); + + if (!p_constant->initializer->is_constant) { + push_error(vformat(R"(Assigned value for constant "%s" isn't a constant expression.)", p_constant->identifier->name), p_constant->initializer); + } + + type = p_constant->initializer->get_datatype(); + +#ifdef DEBUG_ENABLED + if (p_constant->initializer->type == GDScriptParser::Node::CALL && type.kind == GDScriptParser::DataType::BUILTIN && type.builtin_type == Variant::NIL) { + parser->push_warning(p_constant->initializer, GDScriptWarning::VOID_ASSIGNMENT, static_cast<GDScriptParser::CallNode *>(p_constant->initializer)->function_name); + } +#endif + + if (p_constant->datatype_specifier != nullptr) { + GDScriptParser::DataType explicit_type = resolve_datatype(p_constant->datatype_specifier); + explicit_type.is_meta_type = false; + if (!is_type_compatible(explicit_type, type)) { + push_error(vformat(R"(Assigned value for constant "%s" has type %s which is not compatible with defined type %s.)", p_constant->identifier->name, type.to_string(), explicit_type.to_string()), p_constant->initializer); +#ifdef DEBUG_ENABLED + } else if (explicit_type.builtin_type == Variant::INT && type.builtin_type == Variant::FLOAT) { + parser->push_warning(p_constant->initializer, GDScriptWarning::NARROWING_CONVERSION); +#endif + } + type = explicit_type; + } else if (p_constant->infer_datatype) { + if (type.has_no_type()) { + push_error(vformat(R"(Cannot infer the type of constant "%s" because the initial value doesn't have a set type.)", p_constant->identifier->name), p_constant->identifier); + } + type.type_source = GDScriptParser::DataType::ANNOTATED_INFERRED; + } + + type.is_constant = true; + p_constant->set_datatype(type); + +#ifdef DEBUG_ENABLED + if (p_constant->usages == 0) { + parser->push_warning(p_constant, GDScriptWarning::UNUSED_LOCAL_CONSTANT, p_constant->identifier->name); + } + + is_shadowing(p_constant->identifier, "constant"); +#endif +} + +void GDScriptAnalyzer::resolve_assert(GDScriptParser::AssertNode *p_assert) { + reduce_expression(p_assert->condition); + if (p_assert->message != nullptr) { + reduce_literal(p_assert->message); + } + + p_assert->set_datatype(p_assert->condition->get_datatype()); + +#ifdef DEBUG_ENABLED + if (p_assert->condition->is_constant) { + if (p_assert->condition->reduced_value.booleanize()) { + parser->push_warning(p_assert->condition, GDScriptWarning::ASSERT_ALWAYS_TRUE); + } else { + parser->push_warning(p_assert->condition, GDScriptWarning::ASSERT_ALWAYS_FALSE); + } + } +#endif +} + +void GDScriptAnalyzer::resolve_match(GDScriptParser::MatchNode *p_match) { + reduce_expression(p_match->test); + + for (int i = 0; i < p_match->branches.size(); i++) { + resolve_match_branch(p_match->branches[i], p_match->test); + + decide_suite_type(p_match, p_match->branches[i]); + } +} + +void GDScriptAnalyzer::resolve_match_branch(GDScriptParser::MatchBranchNode *p_match_branch, GDScriptParser::ExpressionNode *p_match_test) { + for (int i = 0; i < p_match_branch->patterns.size(); i++) { + resolve_match_pattern(p_match_branch->patterns[i], p_match_test); + } + + resolve_suite(p_match_branch->block); + + decide_suite_type(p_match_branch, p_match_branch->block); +} + +void GDScriptAnalyzer::resolve_match_pattern(GDScriptParser::PatternNode *p_match_pattern, GDScriptParser::ExpressionNode *p_match_test) { + if (p_match_pattern == nullptr) { + return; + } + + GDScriptParser::DataType result; + + switch (p_match_pattern->pattern_type) { + case GDScriptParser::PatternNode::PT_LITERAL: + if (p_match_pattern->literal) { + reduce_literal(p_match_pattern->literal); + result = p_match_pattern->literal->get_datatype(); + } + break; + case GDScriptParser::PatternNode::PT_EXPRESSION: + if (p_match_pattern->expression) { + reduce_expression(p_match_pattern->expression); + if (!p_match_pattern->expression->is_constant) { + push_error(R"(Expression in match pattern must be a constant.)", p_match_pattern->expression); + } + result = p_match_pattern->expression->get_datatype(); + } + break; + case GDScriptParser::PatternNode::PT_BIND: + if (p_match_test != nullptr) { + result = p_match_test->get_datatype(); + } else { + result.kind = GDScriptParser::DataType::VARIANT; + } + p_match_pattern->bind->set_datatype(result); +#ifdef DEBUG_ENABLED + is_shadowing(p_match_pattern->bind, "pattern bind"); + if (p_match_pattern->bind->usages == 0) { + parser->push_warning(p_match_pattern->bind, GDScriptWarning::UNASSIGNED_VARIABLE, p_match_pattern->bind->name); + } +#endif + break; + case GDScriptParser::PatternNode::PT_ARRAY: + for (int i = 0; i < p_match_pattern->array.size(); i++) { + resolve_match_pattern(p_match_pattern->array[i], nullptr); + decide_suite_type(p_match_pattern, p_match_pattern->array[i]); + } + result = p_match_pattern->get_datatype(); + break; + case GDScriptParser::PatternNode::PT_DICTIONARY: + for (int i = 0; i < p_match_pattern->dictionary.size(); i++) { + if (p_match_pattern->dictionary[i].key) { + reduce_expression(p_match_pattern->dictionary[i].key); + if (!p_match_pattern->dictionary[i].key->is_constant) { + push_error(R"(Expression in dictionary pattern key must be a constant.)", p_match_pattern->expression); + } + } + + if (p_match_pattern->dictionary[i].value_pattern) { + resolve_match_pattern(p_match_pattern->dictionary[i].value_pattern, nullptr); + decide_suite_type(p_match_pattern, p_match_pattern->dictionary[i].value_pattern); + } + } + result = p_match_pattern->get_datatype(); + break; + case GDScriptParser::PatternNode::PT_WILDCARD: + case GDScriptParser::PatternNode::PT_REST: + result.kind = GDScriptParser::DataType::VARIANT; + break; + } + + p_match_pattern->set_datatype(result); +} + +void GDScriptAnalyzer::resolve_pararameter(GDScriptParser::ParameterNode *p_parameter) { + GDScriptParser::DataType result; + result.kind = GDScriptParser::DataType::VARIANT; + + if (p_parameter->default_value != nullptr) { + reduce_expression(p_parameter->default_value); + result = p_parameter->default_value->get_datatype(); + result.type_source = GDScriptParser::DataType::INFERRED; + } + + if (p_parameter->datatype_specifier != nullptr) { + resolve_datatype(p_parameter->datatype_specifier); + result = p_parameter->datatype_specifier->get_datatype(); + result.is_meta_type = false; + + if (p_parameter->default_value != nullptr) { + if (!is_type_compatible(result, p_parameter->default_value->get_datatype())) { + push_error(vformat(R"(Type of default value for parameter "%s" (%s) is not compatible with paremeter type (%s).)", p_parameter->identifier->name, p_parameter->default_value->get_datatype().to_string(), p_parameter->datatype_specifier->get_datatype().to_string()), p_parameter->default_value); + } else if (p_parameter->default_value->get_datatype().is_variant()) { + mark_node_unsafe(p_parameter); + } + } + } + + p_parameter->set_datatype(result); +} + +void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) { + GDScriptParser::DataType result; + + if (p_return->return_value != nullptr) { + reduce_expression(p_return->return_value); + result = p_return->return_value->get_datatype(); + } else { + // Return type is null by default. + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::NIL; + result.is_constant = true; + } + + GDScriptParser::DataType function_type = parser->current_function->get_datatype(); + function_type.is_meta_type = false; + if (function_type.is_hard_type()) { + if (!is_type_compatible(function_type, result)) { + // Try other way. Okay but not safe. + if (!is_type_compatible(result, function_type)) { + push_error(vformat(R"(Cannot return value of type "%s" because the function return type is "%s".)", result.to_string(), function_type.to_string()), p_return); + } else { + // TODO: Add warning. + mark_node_unsafe(p_return); + } +#ifdef DEBUG_ENABLED + } else if (function_type.builtin_type == Variant::INT && result.builtin_type == Variant::FLOAT) { + parser->push_warning(p_return, GDScriptWarning::NARROWING_CONVERSION); + } else if (result.is_variant()) { + mark_node_unsafe(p_return); +#endif + } + } + + p_return->set_datatype(result); +} + +void GDScriptAnalyzer::reduce_expression(GDScriptParser::ExpressionNode *p_expression) { + // This one makes some magic happen. + + if (p_expression == nullptr) { + return; + } + + if (p_expression->reduced) { + // Don't do this more than once. + return; + } + + p_expression->reduced = true; + + switch (p_expression->type) { + case GDScriptParser::Node::ARRAY: + reduce_array(static_cast<GDScriptParser::ArrayNode *>(p_expression)); + break; + case GDScriptParser::Node::ASSIGNMENT: + reduce_assignment(static_cast<GDScriptParser::AssignmentNode *>(p_expression)); + break; + case GDScriptParser::Node::AWAIT: + reduce_await(static_cast<GDScriptParser::AwaitNode *>(p_expression)); + break; + case GDScriptParser::Node::BINARY_OPERATOR: + reduce_binary_op(static_cast<GDScriptParser::BinaryOpNode *>(p_expression)); + break; + case GDScriptParser::Node::CALL: + reduce_call(static_cast<GDScriptParser::CallNode *>(p_expression)); + break; + case GDScriptParser::Node::CAST: + reduce_cast(static_cast<GDScriptParser::CastNode *>(p_expression)); + break; + case GDScriptParser::Node::DICTIONARY: + reduce_dictionary(static_cast<GDScriptParser::DictionaryNode *>(p_expression)); + break; + case GDScriptParser::Node::GET_NODE: + reduce_get_node(static_cast<GDScriptParser::GetNodeNode *>(p_expression)); + break; + case GDScriptParser::Node::IDENTIFIER: + reduce_identifier(static_cast<GDScriptParser::IdentifierNode *>(p_expression)); + break; + case GDScriptParser::Node::LITERAL: + reduce_literal(static_cast<GDScriptParser::LiteralNode *>(p_expression)); + break; + case GDScriptParser::Node::PRELOAD: + reduce_preload(static_cast<GDScriptParser::PreloadNode *>(p_expression)); + break; + case GDScriptParser::Node::SELF: + reduce_self(static_cast<GDScriptParser::SelfNode *>(p_expression)); + break; + case GDScriptParser::Node::SUBSCRIPT: + reduce_subscript(static_cast<GDScriptParser::SubscriptNode *>(p_expression)); + break; + case GDScriptParser::Node::TERNARY_OPERATOR: + reduce_ternary_op(static_cast<GDScriptParser::TernaryOpNode *>(p_expression)); + break; + case GDScriptParser::Node::UNARY_OPERATOR: + reduce_unary_op(static_cast<GDScriptParser::UnaryOpNode *>(p_expression)); + break; + // Non-expressions. Here only to make sure new nodes aren't forgotten. + case GDScriptParser::Node::NONE: + case GDScriptParser::Node::ANNOTATION: + case GDScriptParser::Node::ASSERT: + case GDScriptParser::Node::BREAK: + case GDScriptParser::Node::BREAKPOINT: + case GDScriptParser::Node::CLASS: + case GDScriptParser::Node::CONSTANT: + case GDScriptParser::Node::CONTINUE: + case GDScriptParser::Node::ENUM: + case GDScriptParser::Node::FOR: + case GDScriptParser::Node::FUNCTION: + case GDScriptParser::Node::IF: + case GDScriptParser::Node::MATCH: + case GDScriptParser::Node::MATCH_BRANCH: + case GDScriptParser::Node::PARAMETER: + case GDScriptParser::Node::PASS: + case GDScriptParser::Node::PATTERN: + case GDScriptParser::Node::RETURN: + case GDScriptParser::Node::SIGNAL: + case GDScriptParser::Node::SUITE: + case GDScriptParser::Node::TYPE: + case GDScriptParser::Node::VARIABLE: + case GDScriptParser::Node::WHILE: + ERR_FAIL_MSG("Reaching unreachable case"); + } +} + +void GDScriptAnalyzer::reduce_array(GDScriptParser::ArrayNode *p_array) { + bool all_is_constant = true; + + for (int i = 0; i < p_array->elements.size(); i++) { + GDScriptParser::ExpressionNode *element = p_array->elements[i]; + reduce_expression(element); + all_is_constant = all_is_constant && element->is_constant; + } + + if (all_is_constant) { + Array array; + array.resize(p_array->elements.size()); + for (int i = 0; i < p_array->elements.size(); i++) { + array[i] = p_array->elements[i]->reduced_value; + } + p_array->is_constant = true; + p_array->reduced_value = array; + } + + // It's array in any case. + GDScriptParser::DataType arr_type; + arr_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + arr_type.kind = GDScriptParser::DataType::BUILTIN; + arr_type.builtin_type = Variant::ARRAY; + arr_type.is_constant = true; + + p_array->set_datatype(arr_type); +} + +void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assignment) { + reduce_expression(p_assignment->assignee); + reduce_expression(p_assignment->assigned_value); + + if (p_assignment->assigned_value == nullptr || p_assignment->assignee == nullptr) { + return; + } + + if (p_assignment->assignee->get_datatype().is_constant) { + push_error("Cannot assign a new value to a constant.", p_assignment->assignee); + } + + if (!p_assignment->assignee->get_datatype().is_variant() && !p_assignment->assigned_value->get_datatype().is_variant()) { + bool compatible = true; + GDScriptParser::DataType op_type = p_assignment->assigned_value->get_datatype(); + if (p_assignment->operation != GDScriptParser::AssignmentNode::OP_NONE) { + op_type = get_operation_type(p_assignment->variant_op, p_assignment->assignee->get_datatype(), p_assignment->assigned_value->get_datatype(), compatible); + } + + if (compatible) { + compatible = is_type_compatible(p_assignment->assignee->get_datatype(), op_type, true); + if (!compatible) { + if (p_assignment->assignee->get_datatype().is_hard_type()) { + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(op_type, p_assignment->assignee->get_datatype(), true)) { + push_error(vformat(R"(Cannot assign a value of type "%s" to a target of type "%s".)", p_assignment->assigned_value->get_datatype().to_string(), p_assignment->assignee->get_datatype().to_string()), p_assignment->assigned_value); + } else { + // TODO: Add warning. + mark_node_unsafe(p_assignment); + } + } else { + // TODO: Warning in this case. + mark_node_unsafe(p_assignment); + } + } + } else { + push_error(vformat(R"(Invalid operands "%s" and "%s" for assignment operator.)", p_assignment->assignee->get_datatype().to_string(), p_assignment->assigned_value->get_datatype().to_string()), p_assignment); + } + } + + if (p_assignment->assignee->get_datatype().has_no_type() || p_assignment->assigned_value->get_datatype().is_variant()) { + mark_node_unsafe(p_assignment); + } + + if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) { + // Change source type so it's not wrongly detected later. + GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee); + + switch (identifier->source) { + case GDScriptParser::IdentifierNode::MEMBER_VARIABLE: { + GDScriptParser::DataType id_type = identifier->variable_source->get_datatype(); + if (!id_type.is_hard_type()) { + id_type.kind = GDScriptParser::DataType::VARIANT; + id_type.type_source = GDScriptParser::DataType::UNDETECTED; + identifier->variable_source->set_datatype(id_type); + } + } break; + case GDScriptParser::IdentifierNode::LOCAL_VARIABLE: { + GDScriptParser::DataType id_type = identifier->variable_source->get_datatype(); + if (!id_type.is_hard_type()) { + id_type = p_assignment->assigned_value->get_datatype(); + id_type.type_source = GDScriptParser::DataType::INFERRED; + id_type.is_constant = false; + identifier->variable_source->set_datatype(id_type); + } + } break; + case GDScriptParser::IdentifierNode::LOCAL_ITERATOR: { + GDScriptParser::DataType id_type = identifier->bind_source->get_datatype(); + if (!id_type.is_hard_type()) { + id_type = p_assignment->assigned_value->get_datatype(); + id_type.type_source = GDScriptParser::DataType::INFERRED; + id_type.is_constant = false; + identifier->variable_source->set_datatype(id_type); + } + } break; + default: + // Nothing to do. + break; + } + } + + GDScriptParser::DataType assignee_type = p_assignment->assignee->get_datatype(); + GDScriptParser::DataType assigned_type = p_assignment->assigned_value->get_datatype(); +#ifdef DEBUG_ENABLED + if (p_assignment->assigned_value->type == GDScriptParser::Node::CALL && assigned_type.kind == GDScriptParser::DataType::BUILTIN && assigned_type.builtin_type == Variant::NIL) { + parser->push_warning(p_assignment->assigned_value, GDScriptWarning::VOID_ASSIGNMENT, static_cast<GDScriptParser::CallNode *>(p_assignment->assigned_value)->function_name); + } else if (assignee_type.is_hard_type() && assignee_type.builtin_type == Variant::INT && assigned_type.builtin_type == Variant::FLOAT) { + parser->push_warning(p_assignment->assigned_value, GDScriptWarning::NARROWING_CONVERSION); + } +#endif +} + +void GDScriptAnalyzer::reduce_await(GDScriptParser::AwaitNode *p_await) { + if (p_await->to_await == nullptr) { + GDScriptParser::DataType await_type; + await_type.kind = GDScriptParser::DataType::VARIANT; + p_await->set_datatype(await_type); + return; + } + if (p_await->to_await->type == GDScriptParser::Node::CALL) { + reduce_call(static_cast<GDScriptParser::CallNode *>(p_await->to_await), true); + } else { + reduce_expression(p_await->to_await); + } + + p_await->is_constant = p_await->to_await->is_constant; + p_await->reduced_value = p_await->to_await->reduced_value; + + GDScriptParser::DataType awaiting_type = p_await->to_await->get_datatype(); + + p_await->set_datatype(awaiting_type); + +#ifdef DEBUG_ENABLED + if (!awaiting_type.is_coroutine && awaiting_type.builtin_type != Variant::SIGNAL) { + parser->push_warning(p_await, GDScriptWarning::REDUNDANT_AWAIT); + } +#endif +} + +void GDScriptAnalyzer::reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_op) { + reduce_expression(p_binary_op->left_operand); + + if (p_binary_op->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST && p_binary_op->right_operand && p_binary_op->right_operand->type == GDScriptParser::Node::IDENTIFIER) { + reduce_identifier(static_cast<GDScriptParser::IdentifierNode *>(p_binary_op->right_operand), true); + } else { + reduce_expression(p_binary_op->right_operand); + } + // TODO: Right operand must be a valid type with the `is` operator. Need to check here. + + GDScriptParser::DataType left_type; + if (p_binary_op->left_operand) { + left_type = p_binary_op->left_operand->get_datatype(); + } + GDScriptParser::DataType right_type; + if (p_binary_op->right_operand) { + right_type = p_binary_op->right_operand->get_datatype(); + } + + if (!left_type.is_set() || !right_type.is_set()) { + return; + } + +#ifdef DEBUG_ENABLED + if (p_binary_op->variant_op == Variant::OP_DIVIDE && left_type.builtin_type == Variant::INT && right_type.builtin_type == Variant::INT) { + parser->push_warning(p_binary_op, GDScriptWarning::INTEGER_DIVISION); + } +#endif + + if (p_binary_op->left_operand->is_constant && p_binary_op->right_operand->is_constant) { + p_binary_op->is_constant = true; + if (p_binary_op->variant_op < Variant::OP_MAX) { + bool valid = false; + Variant::evaluate(p_binary_op->variant_op, p_binary_op->left_operand->reduced_value, p_binary_op->right_operand->reduced_value, p_binary_op->reduced_value, valid); + if (!valid) { + if (p_binary_op->reduced_value.get_type() == Variant::STRING) { + push_error(vformat(R"(%s in operator %s.)", p_binary_op->reduced_value, Variant::get_operator_name(p_binary_op->variant_op)), p_binary_op); + } else { + push_error(vformat(R"(Invalid operands to operator %s, %s and %s.".)", + Variant::get_operator_name(p_binary_op->variant_op), + Variant::get_type_name(p_binary_op->left_operand->reduced_value.get_type()), + Variant::get_type_name(p_binary_op->right_operand->reduced_value.get_type())), + p_binary_op); + } + } + } else { + if (p_binary_op->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST) { + GDScriptParser::DataType test_type = right_type; + test_type.is_meta_type = false; + + if (!is_type_compatible(test_type, p_binary_op->left_operand->get_datatype(), false)) { + push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)"), p_binary_op->left_operand); + p_binary_op->reduced_value = false; + } else { + p_binary_op->reduced_value = true; + } + } else { + ERR_PRINT("Parser bug: unknown binary operation."); + } + } + p_binary_op->set_datatype(type_from_variant(p_binary_op->reduced_value)); + + return; + } + + GDScriptParser::DataType result; + + if (left_type.is_variant() || right_type.is_variant()) { + // Cannot infer type because one operand can be anything. + result.kind = GDScriptParser::DataType::VARIANT; + mark_node_unsafe(p_binary_op); + } else { + if (p_binary_op->variant_op < Variant::OP_MAX) { + bool valid = false; + result = get_operation_type(p_binary_op->variant_op, p_binary_op->left_operand->get_datatype(), right_type, valid); + + if (!valid) { + push_error(vformat(R"(Invalid operands "%s" and "%s" for "%s" operator.)", p_binary_op->left_operand->get_datatype().to_string(), right_type.to_string(), Variant::get_operator_name(p_binary_op->variant_op)), p_binary_op); + } + } else { + if (p_binary_op->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST) { + GDScriptParser::DataType test_type = right_type; + test_type.is_meta_type = false; + + if (!is_type_compatible(test_type, p_binary_op->left_operand->get_datatype(), false)) { + // Test reverse as well to consider for subtypes. + if (!is_type_compatible(p_binary_op->left_operand->get_datatype(), test_type, false)) { + if (p_binary_op->left_operand->get_datatype().is_hard_type()) { + push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)", p_binary_op->left_operand->get_datatype().to_string(), test_type.to_string()), p_binary_op->left_operand); + } else { + // TODO: Warning. + mark_node_unsafe(p_binary_op); + } + } + } + + // "is" operator is always a boolean anyway. + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::BOOL; + } else { + ERR_PRINT("Parser bug: unknown binary operation."); + } + } + } + + p_binary_op->set_datatype(result); +} + +void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool is_await) { + bool all_is_constant = true; + for (int i = 0; i < p_call->arguments.size(); i++) { + reduce_expression(p_call->arguments[i]); + all_is_constant = all_is_constant && p_call->arguments[i]->is_constant; + } + + GDScriptParser::Node::Type callee_type = p_call->get_callee_type(); + GDScriptParser::DataType call_type; + + if (!p_call->is_super && callee_type == GDScriptParser::Node::IDENTIFIER) { + // Call to name directly. + StringName function_name = p_call->function_name; + Variant::Type builtin_type = GDScriptParser::get_builtin_type(function_name); + GDScriptFunctions::Function builtin_function = GDScriptParser::get_builtin_function(function_name); + + if (builtin_type < Variant::VARIANT_MAX) { + // Is a builtin constructor. + call_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + call_type.kind = GDScriptParser::DataType::BUILTIN; + call_type.builtin_type = builtin_type; + + if (builtin_type == Variant::OBJECT) { + call_type.kind = GDScriptParser::DataType::NATIVE; + call_type.native_type = function_name; // "Object". + } + + if (all_is_constant) { + // Construct here. + Vector<const Variant *> args; + for (int i = 0; i < p_call->arguments.size(); i++) { + args.push_back(&(p_call->arguments[i]->reduced_value)); + } + + Callable::CallError err; + Variant value = Variant::construct(builtin_type, (const Variant **)args.ptr(), args.size(), err); + + switch (err.error) { + case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: + push_error(vformat(R"(Invalid argument for %s constructor: argument %d should be %s but is %s.)", Variant::get_type_name(builtin_type), err.argument + 1, + Variant::get_type_name(Variant::Type(err.expected)), p_call->arguments[err.argument]->get_datatype().to_string()), + p_call->arguments[err.argument]); + break; + case Callable::CallError::CALL_ERROR_INVALID_METHOD: { + String signature = Variant::get_type_name(builtin_type) + "("; + for (int i = 0; i < p_call->arguments.size(); i++) { + if (i > 0) { + signature += ", "; + } + signature += p_call->arguments[i]->get_datatype().to_string(); + } + signature += ")"; + push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call->callee); + } break; + case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS: + push_error(vformat(R"(Too many arguments for %s constructor. Received %d but expected %d.)", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call); + break; + case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS: + push_error(vformat(R"(Too few arguments for %s constructor. Received %d but expected %d.)", Variant::get_type_name(builtin_type), p_call->arguments.size(), err.expected), p_call); + break; + case Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL: + break; // Can't happen in a builtin constructor. + case Callable::CallError::CALL_OK: + p_call->is_constant = true; + p_call->reduced_value = value; + break; + } + } else { + // TODO: Check constructors without constants. + + // If there's one argument, try to use copy constructor (those aren't explicitly defined). + if (p_call->arguments.size() == 1) { + GDScriptParser::DataType arg_type = p_call->arguments[0]->get_datatype(); + if (arg_type.is_variant()) { + mark_node_unsafe(p_call->arguments[0]); + } else { + if (arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == builtin_type) { + // Okay. + p_call->set_datatype(call_type); + return; + } + } + } + List<MethodInfo> constructors; + Variant::get_constructor_list(builtin_type, &constructors); + bool match = false; + + for (const List<MethodInfo>::Element *E = constructors.front(); E != nullptr; E = E->next()) { + const MethodInfo &info = E->get(); + + if (p_call->arguments.size() < info.arguments.size() - info.default_arguments.size()) { + continue; + } + if (p_call->arguments.size() > info.arguments.size()) { + continue; + } + + bool types_match = true; + + for (int i = 0; i < p_call->arguments.size(); i++) { + GDScriptParser::DataType par_type = type_from_property(info.arguments[i]); + + if (!is_type_compatible(par_type, p_call->arguments[i]->get_datatype(), true)) { + types_match = false; + break; +#ifdef DEBUG_ENABLED + } else { + if (par_type.builtin_type == Variant::INT && p_call->arguments[i]->get_datatype().builtin_type == Variant::FLOAT) { + parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name); + } +#endif + } + } + + if (types_match) { + match = true; + call_type = type_from_property(info.return_val); + break; + } + } + + if (!match) { + String signature = Variant::get_type_name(builtin_type) + "("; + for (int i = 0; i < p_call->arguments.size(); i++) { + if (i > 0) { + signature += ", "; + } + signature += p_call->arguments[i]->get_datatype().to_string(); + } + signature += ")"; + push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call); + } + } + p_call->set_datatype(call_type); + return; + } else if (builtin_function < GDScriptFunctions::FUNC_MAX) { + MethodInfo function_info = GDScriptFunctions::get_info(builtin_function); + + if (all_is_constant && GDScriptFunctions::is_deterministic(builtin_function)) { + // Can call on compilation. + Vector<const Variant *> args; + for (int i = 0; i < p_call->arguments.size(); i++) { + args.push_back(&(p_call->arguments[i]->reduced_value)); + } + + Variant value; + Callable::CallError err; + GDScriptFunctions::call(builtin_function, (const Variant **)args.ptr(), args.size(), value, err); + + switch (err.error) { + case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: { + PropertyInfo wrong_arg = function_info.arguments[err.argument]; + push_error(vformat(R"*(Invalid argument for "%s()" function: argument %d should be %s but is %s.)*", GDScriptFunctions::get_func_name(builtin_function), err.argument + 1, + type_from_property(wrong_arg).to_string(), p_call->arguments[err.argument]->get_datatype().to_string()), + p_call->arguments[err.argument]); + } break; + case Callable::CallError::CALL_ERROR_INVALID_METHOD: + push_error(vformat(R"(Invalid call for function "%s".)", GDScriptFunctions::get_func_name(builtin_function)), p_call); + break; + case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS: + push_error(vformat(R"*(Too many arguments for "%s()" call. Expected at most %d but received %d.)*", GDScriptFunctions::get_func_name(builtin_function), err.expected, p_call->arguments.size()), p_call); + break; + case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS: + push_error(vformat(R"*(Too few arguments for "%s()" call. Expected at least %d but received %d.)*", GDScriptFunctions::get_func_name(builtin_function), err.expected, p_call->arguments.size()), p_call); + break; + case Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL: + break; // Can't happen in a builtin constructor. + case Callable::CallError::CALL_OK: + p_call->is_constant = true; + p_call->reduced_value = value; + break; + } + } else { + validate_call_arg(function_info, p_call); + } + p_call->set_datatype(type_from_property(function_info.return_val)); + return; + } + } + + GDScriptParser::DataType base_type; + call_type.kind = GDScriptParser::DataType::VARIANT; + bool is_self = false; + + if (p_call->is_super) { + base_type = parser->current_class->base_type; + is_self = true; + } else if (callee_type == GDScriptParser::Node::IDENTIFIER) { + base_type = parser->current_class->get_datatype(); + is_self = true; + } else if (callee_type == GDScriptParser::Node::SUBSCRIPT) { + GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(p_call->callee); + if (!subscript->is_attribute) { + // Invalid call. Error already sent in parser. + // TODO: Could check if Callable here. + p_call->set_datatype(call_type); + mark_node_unsafe(p_call); + return; + } + reduce_expression(subscript->base); + + base_type = subscript->base->get_datatype(); + } else { + // Invalid call. Error already sent in parser. + // TODO: Could check if Callable here too. + p_call->set_datatype(call_type); + mark_node_unsafe(p_call); + return; + } + + bool is_static = false; + bool is_vararg = false; + int default_arg_count = 0; + GDScriptParser::DataType return_type; + List<GDScriptParser::DataType> par_types; + + if (get_function_signature(p_call, base_type, p_call->function_name, return_type, par_types, default_arg_count, is_static, is_vararg)) { + validate_call_arg(par_types, default_arg_count, is_vararg, p_call); + + if (is_self && parser->current_function != nullptr && parser->current_function->is_static && !is_static) { + push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parser->current_function->identifier->name), p_call->callee); + } + + call_type = return_type; + } else { + // Check if the name exists as something else. + bool found = false; + if (!p_call->is_super && callee_type != GDScriptParser::Node::NONE) { + GDScriptParser::IdentifierNode *callee_id; + if (callee_type == GDScriptParser::Node::IDENTIFIER) { + callee_id = static_cast<GDScriptParser::IdentifierNode *>(p_call->callee); + } else { + // Can only be attribute. + callee_id = static_cast<GDScriptParser::SubscriptNode *>(p_call->callee)->attribute; + } + if (callee_id) { + reduce_identifier_from_base(callee_id, &base_type); + GDScriptParser::DataType callee_datatype = callee_id->get_datatype(); + if (callee_datatype.is_set() && !callee_datatype.is_variant()) { + found = true; + if (callee_datatype.builtin_type == Variant::CALLABLE) { + push_error(vformat(R"*(Name "%s" is a Callable. You can call it with "%s.call()" instead.)*", p_call->function_name, p_call->function_name), p_call->callee); + } else { + push_error(vformat(R"*(Name "%s" called as a function but is a "%s".)*", p_call->function_name, callee_datatype.to_string()), p_call->callee); + } +#ifdef DEBUG_ENABLED + } else if (!is_self) { + parser->push_warning(p_call, GDScriptWarning::UNSAFE_METHOD_ACCESS, p_call->function_name, base_type.to_string()); + mark_node_unsafe(p_call); +#endif + } + } + } + if (!found && is_self) { + String base_name = is_self && !p_call->is_super ? "self" : base_type.to_string(); + push_error(vformat(R"*(Function "%s()" not found in base %s.)*", p_call->function_name, base_name), p_call->is_super ? p_call : p_call->callee); + } + } + + if (call_type.is_coroutine && !is_await) { + push_error(vformat(R"*(Function "%s()" is a coroutine, so it must be called with "await".)*", p_call->function_name), p_call->callee); + } + + p_call->set_datatype(call_type); +} + +void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { + reduce_expression(p_cast->operand); + + GDScriptParser::DataType cast_type = resolve_datatype(p_cast->cast_type); + + if (!cast_type.is_set()) { + return; + } + + cast_type.is_meta_type = false; // The casted value won't be a type name. + p_cast->set_datatype(cast_type); + + if (!cast_type.is_variant()) { + GDScriptParser::DataType op_type = p_cast->operand->get_datatype(); + if (!op_type.is_variant()) { + bool valid = false; + if (op_type.kind == GDScriptParser::DataType::BUILTIN && cast_type.kind == GDScriptParser::DataType::BUILTIN) { + valid = Variant::can_convert(op_type.builtin_type, cast_type.builtin_type); + } else if (op_type.kind != GDScriptParser::DataType::BUILTIN && cast_type.kind != GDScriptParser::DataType::BUILTIN) { + valid = is_type_compatible(cast_type, op_type) || is_type_compatible(op_type, cast_type); + } + + if (!valid) { + push_error(vformat(R"(Invalid cast. Cannot convert from "%s" to "%s".)", op_type.to_string(), cast_type.to_string()), p_cast->cast_type); + } + } + } else { + mark_node_unsafe(p_cast); + } +#ifdef DEBUG_ENABLED + if (p_cast->operand->get_datatype().is_variant()) { + parser->push_warning(p_cast, GDScriptWarning::UNSAFE_CAST, cast_type.to_string()); + mark_node_unsafe(p_cast); + } +#endif + + // TODO: Perform cast on constants. +} + +void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) { + bool all_is_constant = true; + + HashMap<Variant, GDScriptParser::ExpressionNode *, VariantHasher, VariantComparator> elements; + + for (int i = 0; i < p_dictionary->elements.size(); i++) { + const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i]; + if (p_dictionary->style == GDScriptParser::DictionaryNode::PYTHON_DICT) { + reduce_expression(element.key); + } + reduce_expression(element.value); + all_is_constant = all_is_constant && element.key->is_constant && element.value->is_constant; + + if (element.key->is_constant) { + if (elements.has(element.key->reduced_value)) { + push_error(vformat(R"(Key "%s" was already used in this dictionary (at line %d).)", element.key->reduced_value, elements[element.key->reduced_value]->start_line), element.key); + } else { + elements[element.key->reduced_value] = element.value; + } + } + } + + if (all_is_constant) { + Dictionary dict; + for (int i = 0; i < p_dictionary->elements.size(); i++) { + const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i]; + dict[element.key->reduced_value] = element.value->reduced_value; + } + p_dictionary->is_constant = true; + p_dictionary->reduced_value = dict; + } + + // It's dictionary in any case. + GDScriptParser::DataType dict_type; + dict_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + dict_type.kind = GDScriptParser::DataType::BUILTIN; + dict_type.builtin_type = Variant::DICTIONARY; + dict_type.is_constant = true; + + p_dictionary->set_datatype(dict_type); +} + +void GDScriptAnalyzer::reduce_get_node(GDScriptParser::GetNodeNode *p_get_node) { + GDScriptParser::DataType result; + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = "Node"; + result.builtin_type = Variant::OBJECT; + + if (!ClassDB::is_parent_class(get_real_class_name(parser->current_class->base_type.native_type), result.native_type)) { + push_error(R"*(Cannot use shorthand "get_node()" notation ("$") on a class that isn't a node.)*", p_get_node); + } + + p_get_node->set_datatype(result); +} + +GDScriptParser::DataType GDScriptAnalyzer::make_global_class_meta_type(const StringName &p_class_name) { + Ref<GDScriptParserRef> ref = get_parser_for(ScriptServer::get_global_class_path(p_class_name)); + ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED); + + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = GDScriptParser::DataType::CLASS; + type.builtin_type = Variant::OBJECT; + type.native_type = ScriptServer::get_global_class_native_base(p_class_name); + type.class_type = ref->get_parser()->head; + type.script_path = ref->get_parser()->script_path; + type.is_constant = true; + type.is_meta_type = true; + return type; +} + +void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNode *p_identifier, GDScriptParser::DataType *p_base) { + GDScriptParser::DataType base; + if (p_base == nullptr) { + base = type_from_metatype(parser->current_class->get_datatype()); + } else { + base = *p_base; + } + + const StringName &name = p_identifier->name; + + if (base.kind == GDScriptParser::DataType::BUILTIN) { + if (base.is_meta_type) { + bool valid = true; + Variant result = Variant::get_constant_value(base.builtin_type, name, &valid); + if (valid) { + p_identifier->is_constant = true; + p_identifier->reduced_value = result; + p_identifier->set_datatype(type_from_variant(result)); + } else { + push_error(vformat(R"(Cannot find constant "%s" on type "%s".)", name, base.to_string()), p_identifier); + } + } else { + Callable::CallError temp; + Variant dummy = Variant::construct(base.builtin_type, nullptr, 0, temp); + List<PropertyInfo> properties; + dummy.get_property_list(&properties); + for (const List<PropertyInfo>::Element *E = properties.front(); E != nullptr; E = E->next()) { + const PropertyInfo &prop = E->get(); + if (prop.name == name) { + p_identifier->set_datatype(type_from_property(prop)); + return; + } + } + push_error(vformat(R"(Cannot find property "%s" on base "%s".)", name, base.to_string()), p_identifier); + } + return; + } + + if (base.kind == GDScriptParser::DataType::ENUM) { + if (base.is_meta_type) { + if (base.enum_values.has(name)) { + p_identifier->is_constant = true; + p_identifier->reduced_value = base.enum_values[name]; + + GDScriptParser::DataType result; + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::ENUM_VALUE; + result.native_type = base.native_type; + result.enum_type = name; + p_identifier->set_datatype(result); + } else { + push_error(vformat(R"(Cannot find value "%s" in "%s".)", name, base.to_string()), p_identifier); + } + } else { + push_error(R"(Cannot get property from enum value.)", p_identifier); + } + return; + } + + GDScriptParser::ClassNode *base_class = base.class_type; + + // TODO: Switch current class/function/suite here to avoid misrepresenting identifiers (in recursive reduce calls). + while (base_class != nullptr) { + if (base_class->identifier && base_class->identifier->name == name) { + p_identifier->set_datatype(base_class->get_datatype()); + return; + } + if (base_class->has_member(name)) { + const GDScriptParser::ClassNode::Member &member = base_class->get_member(name); + p_identifier->set_datatype(member.get_datatype()); + switch (member.type) { + case GDScriptParser::ClassNode::Member::CONSTANT: + // For out-of-order resolution: + reduce_expression(member.constant->initializer); + p_identifier->is_constant = true; + p_identifier->reduced_value = member.constant->initializer->reduced_value; + p_identifier->set_datatype(member.constant->initializer->get_datatype()); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CONSTANT; + p_identifier->constant_source = member.constant; + break; + case GDScriptParser::ClassNode::Member::ENUM_VALUE: + p_identifier->is_constant = true; + p_identifier->reduced_value = member.enum_value.value; + break; + case GDScriptParser::ClassNode::Member::VARIABLE: + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_VARIABLE; + p_identifier->variable_source = member.variable; + break; + case GDScriptParser::ClassNode::Member::FUNCTION: + resolve_function_signature(member.function); + p_identifier->set_datatype(make_callable_type(member.function->info)); + break; + default: + break; // Type already set. + } + return; + } + // Check outer constants. + // TODO: Allow outer static functions. + GDScriptParser::ClassNode *outer = base_class->outer; + while (outer != nullptr) { + if (outer->has_member(name)) { + const GDScriptParser::ClassNode::Member &member = outer->get_member(name); + if (member.type == GDScriptParser::ClassNode::Member::CONSTANT) { + // TODO: Make sure loops won't cause problem. And make special error message for those. + // For out-of-order resolution: + reduce_expression(member.constant->initializer); + p_identifier->set_datatype(member.get_datatype()); + p_identifier->is_constant = true; + p_identifier->reduced_value = member.constant->initializer->reduced_value; + return; + } + } + outer = outer->outer; + } + + base_class = base_class->base_type.class_type; + } + + // Check native members. + const StringName &native = get_real_class_name(base.native_type); + + if (class_exists(native)) { + PropertyInfo prop_info; + MethodInfo method_info; + if (ClassDB::get_property_info(native, name, &prop_info)) { + p_identifier->set_datatype(type_from_property(prop_info)); + return; + } + if (ClassDB::get_method_info(native, name, &method_info)) { + // Method is callable. + p_identifier->set_datatype(make_callable_type(method_info)); + return; + } + if (ClassDB::get_signal(native, name, &method_info)) { + // Signal is a type too. + p_identifier->set_datatype(make_signal_type(method_info)); + return; + } + if (ClassDB::has_enum(native, name)) { + p_identifier->set_datatype(make_native_enum_type(native, name)); + return; + } + bool valid = false; + int int_constant = ClassDB::get_integer_constant(native, name, &valid); + if (valid) { + p_identifier->is_constant = true; + p_identifier->reduced_value = int_constant; + p_identifier->set_datatype(type_from_variant(int_constant)); + return; + } + } +} + +void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_identifier, bool can_be_builtin) { + // TODO: This is opportunity to further infer types. + + // Check if we are inside and enum. This allows enum values to access other elements of the same enum. + if (current_enum) { + for (int i = 0; i < current_enum->values.size(); i++) { + const GDScriptParser::EnumNode::Value &element = current_enum->values[i]; + if (element.identifier->name == p_identifier->name) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = element.parent_enum->identifier ? GDScriptParser::DataType::ENUM_VALUE : GDScriptParser::DataType::BUILTIN; + type.builtin_type = Variant::INT; + type.is_constant = true; + if (element.parent_enum->identifier) { + type.enum_type = element.parent_enum->identifier->name; + } + p_identifier->set_datatype(type); + + if (element.resolved) { + p_identifier->is_constant = true; + p_identifier->reduced_value = element.value; + } else { + push_error(R"(Cannot use another enum element before it was declared.)", p_identifier); + } + return; // Found anyway. + } + } + } + + // Check if identifier is local. + // If that's the case, the declaration already was solved before. + switch (p_identifier->source) { + case GDScriptParser::IdentifierNode::FUNCTION_PARAMETER: + p_identifier->set_datatype(p_identifier->parameter_source->get_datatype()); + return; + case GDScriptParser::IdentifierNode::LOCAL_CONSTANT: + case GDScriptParser::IdentifierNode::MEMBER_CONSTANT: + p_identifier->set_datatype(p_identifier->constant_source->get_datatype()); + p_identifier->is_constant = true; + // TODO: Constant should have a value on the node itself. + p_identifier->reduced_value = p_identifier->constant_source->initializer->reduced_value; + return; + case GDScriptParser::IdentifierNode::MEMBER_VARIABLE: + p_identifier->variable_source->usages++; + [[fallthrough]]; + case GDScriptParser::IdentifierNode::LOCAL_VARIABLE: + p_identifier->set_datatype(p_identifier->variable_source->get_datatype()); + return; + case GDScriptParser::IdentifierNode::LOCAL_ITERATOR: + p_identifier->set_datatype(p_identifier->bind_source->get_datatype()); + return; + case GDScriptParser::IdentifierNode::LOCAL_BIND: { + GDScriptParser::DataType result = p_identifier->bind_source->get_datatype(); + result.is_constant = true; + p_identifier->set_datatype(result); + return; + } + case GDScriptParser::IdentifierNode::UNDEFINED_SOURCE: + break; + } + + // Not a local, so check members. + reduce_identifier_from_base(p_identifier); + if (p_identifier->get_datatype().is_set()) { + // Found. + return; + } + + StringName name = p_identifier->name; + p_identifier->source = GDScriptParser::IdentifierNode::UNDEFINED_SOURCE; + + // Check globals. + if (GDScriptParser::get_builtin_type(name) < Variant::VARIANT_MAX) { + if (can_be_builtin) { + p_identifier->set_datatype(make_builtin_meta_type(GDScriptParser::get_builtin_type(name))); + return; + } else { + push_error(R"(Builtin type cannot be used as a name on its own.)", p_identifier); + } + } + + if (class_exists(name)) { + p_identifier->set_datatype(make_native_meta_type(name)); + return; + } + + if (ScriptServer::is_global_class(name)) { + p_identifier->set_datatype(make_global_class_meta_type(name)); + return; + } + + if (GDScriptLanguage::get_singleton()->get_global_map().has(name)) { + int idx = GDScriptLanguage::get_singleton()->get_global_map()[name]; + Variant constant = GDScriptLanguage::get_singleton()->get_global_array()[idx]; + p_identifier->set_datatype(type_from_variant(constant)); + p_identifier->is_constant = true; + p_identifier->reduced_value = constant; + return; + } + + if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(name)) { + Variant constant = GDScriptLanguage::get_singleton()->get_named_globals_map()[name]; + p_identifier->set_datatype(type_from_variant(constant)); + p_identifier->is_constant = true; + p_identifier->reduced_value = constant; + return; + } + + // Not found. + // Check if it's a builtin function. + if (parser->get_builtin_function(name) < GDScriptFunctions::FUNC_MAX) { + push_error(vformat(R"(Built-in function "%s" cannot be used as an identifier.)", name), p_identifier); + } else { + push_error(vformat(R"(Identifier "%s" not declared in the current scope.)", name), p_identifier); + } + GDScriptParser::DataType dummy; + dummy.kind = GDScriptParser::DataType::VARIANT; + p_identifier->set_datatype(dummy); // Just so type is set to something. +} + +void GDScriptAnalyzer::reduce_literal(GDScriptParser::LiteralNode *p_literal) { + p_literal->reduced_value = p_literal->value; + p_literal->is_constant = true; + + p_literal->set_datatype(type_from_variant(p_literal->reduced_value)); +} + +void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { + if (!p_preload->path) { + return; + } + + reduce_expression(p_preload->path); + + if (!p_preload->path->is_constant) { + push_error("Preloaded path must be a constant string.", p_preload->path); + return; + } + + if (p_preload->path->reduced_value.get_type() != Variant::STRING) { + push_error("Preloaded path must be a constant string.", p_preload->path); + } else { + p_preload->resolved_path = p_preload->path->reduced_value; + // TODO: Save this as script dependency. + if (p_preload->resolved_path.is_rel_path()) { + p_preload->resolved_path = parser->script_path.get_base_dir().plus_file(p_preload->resolved_path); + } + p_preload->resolved_path = p_preload->resolved_path.simplify_path(); + if (!FileAccess::exists(p_preload->resolved_path)) { + push_error(vformat(R"(Preload file "%s" does not exist.)", p_preload->resolved_path), p_preload->path); + } else { + // TODO: Don't load if validating: use completion cache. + p_preload->resource = ResourceLoader::load(p_preload->resolved_path); + if (p_preload->resource.is_null()) { + push_error(vformat(R"(Could not p_preload resource file "%s".)", p_preload->resolved_path), p_preload->path); + } + } + } + + p_preload->is_constant = true; + p_preload->reduced_value = p_preload->resource; + p_preload->set_datatype(type_from_variant(p_preload->reduced_value)); +} + +void GDScriptAnalyzer::reduce_self(GDScriptParser::SelfNode *p_self) { + p_self->is_constant = false; + p_self->set_datatype(type_from_metatype(parser->current_class->get_datatype())); +} + +void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscript) { + if (p_subscript->base->type == GDScriptParser::Node::IDENTIFIER) { + reduce_identifier(static_cast<GDScriptParser::IdentifierNode *>(p_subscript->base), true); + } else { + reduce_expression(p_subscript->base); + } + + GDScriptParser::DataType result_type; + + // Reduce index first. If it's a constant StringName, use attribute instead. + if (!p_subscript->is_attribute) { + if (p_subscript->index == nullptr) { + return; + } + reduce_expression(p_subscript->index); + + if (p_subscript->index->is_constant && p_subscript->index->reduced_value.get_type() == Variant::STRING_NAME) { + GDScriptParser::IdentifierNode *attribute = parser->alloc_node<GDScriptParser::IdentifierNode>(); + // Copy location for better error message. + attribute->start_line = p_subscript->index->start_line; + attribute->end_line = p_subscript->index->end_line; + attribute->leftmost_column = p_subscript->index->leftmost_column; + attribute->rightmost_column = p_subscript->index->rightmost_column; + p_subscript->is_attribute = true; + p_subscript->attribute = attribute; + } + } + + if (p_subscript->is_attribute) { + if (p_subscript->attribute == nullptr) { + return; + } + if (p_subscript->base->is_constant) { + // Just try to get it. + bool valid = false; + Variant value = p_subscript->base->reduced_value.get_named(p_subscript->attribute->name, &valid); + if (!valid) { + push_error(vformat(R"(Cannot get member "%s" from "%s".)", p_subscript->attribute->name, p_subscript->base->reduced_value), p_subscript->index); + } else { + p_subscript->is_constant = true; + p_subscript->reduced_value = value; + result_type = type_from_variant(value); + } + result_type.kind = GDScriptParser::DataType::VARIANT; + } else { + GDScriptParser::DataType base_type = p_subscript->base->get_datatype(); + + if (base_type.is_variant()) { + result_type.kind = GDScriptParser::DataType::VARIANT; + mark_node_unsafe(p_subscript); + } else { + reduce_identifier_from_base(p_subscript->attribute, &base_type); + GDScriptParser::DataType attr_type = p_subscript->attribute->get_datatype(); + if (attr_type.is_set()) { + result_type = attr_type; + p_subscript->is_constant = p_subscript->attribute->is_constant; + p_subscript->reduced_value = p_subscript->attribute->reduced_value; + } else { + if (base_type.kind == GDScriptParser::DataType::BUILTIN) { + push_error(vformat(R"(Cannot find member "%s" in base "%s".)", p_subscript->attribute->name, base_type.to_string()), p_subscript->attribute); +#ifdef DEBUG_ENABLED + } else { + parser->push_warning(p_subscript, GDScriptWarning::UNSAFE_PROPERTY_ACCESS, p_subscript->attribute->name, base_type.to_string()); +#endif + } + result_type.kind = GDScriptParser::DataType::VARIANT; + } + } + } + } else { + // Index was already reduced before. + + if (p_subscript->base->is_constant && p_subscript->index->is_constant) { + // Just try to get it. + bool valid = false; + Variant value = p_subscript->base->reduced_value.get(p_subscript->index->reduced_value, &valid); + if (!valid) { + push_error(vformat(R"(Cannot get index "%s" from "%s".)", p_subscript->index->reduced_value, p_subscript->base->reduced_value), p_subscript->index); + } else { + p_subscript->is_constant = true; + p_subscript->reduced_value = value; + result_type = type_from_variant(value); + } + result_type.kind = GDScriptParser::DataType::VARIANT; + } else { + GDScriptParser::DataType base_type = p_subscript->base->get_datatype(); + GDScriptParser::DataType index_type = p_subscript->index->get_datatype(); + + if (base_type.is_variant()) { + result_type.kind = GDScriptParser::DataType::VARIANT; + mark_node_unsafe(p_subscript); + } else { + if (base_type.kind == GDScriptParser::DataType::BUILTIN && !index_type.is_variant()) { + // Check if indexing is valid. + bool error = index_type.kind != GDScriptParser::DataType::BUILTIN && base_type.builtin_type != Variant::DICTIONARY; + if (!error) { + switch (base_type.builtin_type) { + // Expect int or real as index. + case Variant::PACKED_BYTE_ARRAY: + case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_FLOAT32_ARRAY: + case Variant::PACKED_FLOAT64_ARRAY: + case Variant::PACKED_INT32_ARRAY: + case Variant::PACKED_INT64_ARRAY: + case Variant::PACKED_STRING_ARRAY: + case Variant::PACKED_VECTOR2_ARRAY: + case Variant::PACKED_VECTOR3_ARRAY: + case Variant::ARRAY: + case Variant::STRING: + error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT; + break; + // Expect String only. + case Variant::RECT2: + case Variant::RECT2I: + case Variant::PLANE: + case Variant::QUAT: + case Variant::AABB: + case Variant::OBJECT: + error = index_type.builtin_type != Variant::STRING; + break; + // Expect String or number. + case Variant::BASIS: + case Variant::VECTOR2: + case Variant::VECTOR2I: + case Variant::VECTOR3: + case Variant::VECTOR3I: + case Variant::TRANSFORM: + case Variant::TRANSFORM2D: + error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT && + index_type.builtin_type != Variant::STRING; + break; + // Expect String or int. + case Variant::COLOR: + error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::STRING; + break; + // Don't support indexing, but we will check it later. + case Variant::_RID: + case Variant::BOOL: + case Variant::CALLABLE: + case Variant::FLOAT: + case Variant::INT: + case Variant::NIL: + case Variant::NODE_PATH: + case Variant::SIGNAL: + case Variant::STRING_NAME: + break; + // Here for completeness. + case Variant::DICTIONARY: + case Variant::VARIANT_MAX: + break; + } + + if (error) { + push_error(vformat(R"(Invalid index type "%s" for a base of type "%s".)", index_type.to_string(), base_type.to_string()), p_subscript->index); + } + } + } else if (base_type.kind != GDScriptParser::DataType::BUILTIN && !index_type.is_variant()) { + if (index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME) { + push_error(vformat(R"(Only String or StringName can be used as index for type "%s", but received a "%s".)", base_type.to_string(), index_type.to_string()), p_subscript->index); + } + } + + // Check resulting type if possible. + result_type.builtin_type = Variant::NIL; + result_type.kind = GDScriptParser::DataType::BUILTIN; + result_type.type_source = base_type.is_hard_type() ? GDScriptParser::DataType::ANNOTATED_INFERRED : GDScriptParser::DataType::INFERRED; + + switch (base_type.builtin_type) { + // Can't index at all. + case Variant::_RID: + case Variant::BOOL: + case Variant::CALLABLE: + case Variant::FLOAT: + case Variant::INT: + case Variant::NIL: + case Variant::NODE_PATH: + case Variant::SIGNAL: + case Variant::STRING_NAME: + result_type.kind = GDScriptParser::DataType::VARIANT; + push_error(vformat(R"(Cannot use subscript operator on a base of type "%s".)", base_type.to_string()), p_subscript->base); + break; + // Return int. + case Variant::PACKED_BYTE_ARRAY: + case Variant::PACKED_INT32_ARRAY: + case Variant::PACKED_INT64_ARRAY: + case Variant::VECTOR2I: + case Variant::VECTOR3I: + result_type.builtin_type = Variant::INT; + break; + // Return float. + case Variant::PACKED_FLOAT32_ARRAY: + case Variant::PACKED_FLOAT64_ARRAY: + case Variant::VECTOR2: + case Variant::VECTOR3: + case Variant::QUAT: + result_type.builtin_type = Variant::FLOAT; + break; + // Return Color. + case Variant::PACKED_COLOR_ARRAY: + result_type.builtin_type = Variant::COLOR; + break; + // Return String. + case Variant::PACKED_STRING_ARRAY: + case Variant::STRING: + result_type.builtin_type = Variant::STRING; + break; + // Return Vector2. + case Variant::PACKED_VECTOR2_ARRAY: + case Variant::TRANSFORM2D: + case Variant::RECT2: + result_type.builtin_type = Variant::VECTOR2; + break; + // Return Vector2I. + case Variant::RECT2I: + result_type.builtin_type = Variant::VECTOR2I; + break; + // Return Vector3. + case Variant::PACKED_VECTOR3_ARRAY: + case Variant::AABB: + case Variant::BASIS: + result_type.builtin_type = Variant::VECTOR3; + break; + // Depends on the index. + case Variant::TRANSFORM: + case Variant::PLANE: + case Variant::COLOR: + case Variant::ARRAY: + case Variant::DICTIONARY: + result_type.kind = GDScriptParser::DataType::VARIANT; + result_type.type_source = GDScriptParser::DataType::UNDETECTED; + break; + // Here for completeness. + case Variant::OBJECT: + case Variant::VARIANT_MAX: + break; + } + } + } + } + + p_subscript->set_datatype(result_type); +} + +void GDScriptAnalyzer::reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternary_op) { + reduce_expression(p_ternary_op->condition); + reduce_expression(p_ternary_op->true_expr); + reduce_expression(p_ternary_op->false_expr); + + GDScriptParser::DataType result; + + if (p_ternary_op->condition && p_ternary_op->condition->is_constant && p_ternary_op->true_expr->is_constant && p_ternary_op->false_expr && p_ternary_op->false_expr->is_constant) { + p_ternary_op->is_constant = true; + if (p_ternary_op->condition->reduced_value.booleanize()) { + p_ternary_op->reduced_value = p_ternary_op->true_expr->reduced_value; + } else { + p_ternary_op->reduced_value = p_ternary_op->false_expr->reduced_value; + } + } + + GDScriptParser::DataType true_type; + if (p_ternary_op->true_expr) { + true_type = p_ternary_op->true_expr->get_datatype(); + } else { + true_type.kind = GDScriptParser::DataType::VARIANT; + } + GDScriptParser::DataType false_type; + if (p_ternary_op->false_expr) { + false_type = p_ternary_op->false_expr->get_datatype(); + } else { + false_type.kind = GDScriptParser::DataType::VARIANT; + } + + if (true_type.is_variant() || false_type.is_variant()) { + result.kind = GDScriptParser::DataType::VARIANT; + } else { + result = true_type; + if (!is_type_compatible(true_type, false_type)) { + result = false_type; + if (!is_type_compatible(false_type, true_type)) { + result.type_source = GDScriptParser::DataType::UNDETECTED; + result.kind = GDScriptParser::DataType::VARIANT; +#ifdef DEBUG_ENABLED + parser->push_warning(p_ternary_op, GDScriptWarning::INCOMPATIBLE_TERNARY); +#endif + } + } + } + + p_ternary_op->set_datatype(result); +} + +void GDScriptAnalyzer::reduce_unary_op(GDScriptParser::UnaryOpNode *p_unary_op) { + reduce_expression(p_unary_op->operand); + + GDScriptParser::DataType result; + + if (p_unary_op->operand == nullptr) { + result.kind = GDScriptParser::DataType::VARIANT; + p_unary_op->set_datatype(result); + return; + } + + if (p_unary_op->operand->is_constant) { + p_unary_op->is_constant = true; + p_unary_op->reduced_value = Variant::evaluate(p_unary_op->variant_op, p_unary_op->operand->reduced_value, Variant()); + result = type_from_variant(p_unary_op->reduced_value); + } else if (p_unary_op->operand->get_datatype().is_variant()) { + result.kind = GDScriptParser::DataType::VARIANT; + mark_node_unsafe(p_unary_op); + } else { + bool valid = false; + result = get_operation_type(p_unary_op->variant_op, p_unary_op->operand->get_datatype(), p_unary_op->operand->get_datatype(), valid); + + if (!valid) { + push_error(vformat(R"(Invalid operand of type "%s" for unary operator "%s".)", p_unary_op->operand->get_datatype().to_string(), Variant::get_operator_name(p_unary_op->variant_op)), p_unary_op->operand); + } + } + + p_unary_op->set_datatype(result); +} + +GDScriptParser::DataType GDScriptAnalyzer::type_from_variant(const Variant &p_value) { + GDScriptParser::DataType result; + result.is_constant = true; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = p_value.get_type(); + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; // Constant has explicit type. + + if (p_value.get_type() == Variant::OBJECT) { + Object *obj = p_value; + if (!obj) { + return GDScriptParser::DataType(); + } + result.native_type = obj->get_class_name(); + + Ref<Script> scr = p_value; // Check if value is a script itself. + if (scr.is_valid()) { + result.is_meta_type = true; + } else { + result.is_meta_type = false; + scr = obj->get_script(); + } + if (scr.is_valid()) { + result.script_type = scr; + result.script_path = scr->get_path(); + Ref<GDScript> gds = scr; + if (gds.is_valid()) { + result.kind = GDScriptParser::DataType::CLASS; + Ref<GDScriptParserRef> ref = get_parser_for(gds->get_path()); + ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED); + result.class_type = ref->get_parser()->head; + result.script_path = ref->get_parser()->script_path; + } else { + result.kind = GDScriptParser::DataType::SCRIPT; + } + result.native_type = scr->get_instance_base_type(); + } else { + result.kind = GDScriptParser::DataType::NATIVE; + if (result.native_type == GDScriptNativeClass::get_class_static()) { + result.is_meta_type = true; + } + } + } + + return result; +} + +GDScriptParser::DataType GDScriptAnalyzer::type_from_metatype(const GDScriptParser::DataType &p_meta_type) const { + GDScriptParser::DataType result = p_meta_type; + result.is_meta_type = false; + result.is_constant = false; + return result; +} + +GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo &p_property) const { + GDScriptParser::DataType result; + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + if (p_property.type == Variant::NIL && (p_property.usage & PROPERTY_USAGE_NIL_IS_VARIANT)) { + // Variant + result.kind = GDScriptParser::DataType::VARIANT; + return result; + } + result.builtin_type = p_property.type; + if (p_property.type == Variant::OBJECT) { + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name; + } else { + result.kind = GDScriptParser::DataType::BUILTIN; + } + return result; +} + +bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, GDScriptParser::DataType p_base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) { + r_static = false; + r_vararg = false; + r_default_arg_count = 0; + StringName function_name = p_function; + + if (p_base_type.kind == GDScriptParser::DataType::BUILTIN) { + // Construct a base type to get methods. + Callable::CallError err; + Variant dummy = Variant::construct(p_base_type.builtin_type, nullptr, 0, err); + if (err.error != Callable::CallError::CALL_OK) { + ERR_FAIL_V_MSG(false, "Could not construct base Variant type."); + } + List<MethodInfo> methods; + dummy.get_method_list(&methods); + + for (const List<MethodInfo>::Element *E = methods.front(); E != nullptr; E = E->next()) { + if (E->get().name == p_function) { + return function_signature_from_info(E->get(), r_return_type, r_par_types, r_default_arg_count, r_static, r_vararg); + } + } + + return false; + } + + bool is_constructor = p_base_type.is_meta_type && p_function == "new"; + if (is_constructor) { + function_name = "_init"; + r_static = true; + } + + GDScriptParser::ClassNode *base_class = p_base_type.class_type; + GDScriptParser::FunctionNode *found_function = nullptr; + + while (found_function == nullptr && base_class != nullptr) { + if (base_class->has_member(function_name)) { + if (base_class->get_member(function_name).type != GDScriptParser::ClassNode::Member::FUNCTION) { + // TODO: If this is Callable it can have a better error message. + push_error(vformat(R"(Member "%s" is not a function.)", function_name), p_source); + return false; + } + found_function = base_class->get_member(function_name).function; + } + base_class = base_class->base_type.class_type; + } + + if (found_function != nullptr) { + r_static = is_constructor || found_function->is_static; + for (int i = 0; i < found_function->parameters.size(); i++) { + r_par_types.push_back(found_function->parameters[i]->get_datatype()); + if (found_function->parameters[i]->default_value != nullptr) { + r_default_arg_count++; + } + } + r_return_type = found_function->get_datatype(); + r_return_type.is_coroutine = found_function->is_coroutine; + + return true; + } + + Ref<Script> base_script = p_base_type.script_type; + + while (base_script.is_valid() && base_script->is_valid()) { + MethodInfo info = base_script->get_method_info(function_name); + + if (!(info == MethodInfo())) { + return function_signature_from_info(info, r_return_type, r_par_types, r_default_arg_count, r_static, r_vararg); + } + base_script = base_script->get_base_script(); + } + + // If the base is a script, it might be trying to access members of the Script class itself. + if (p_base_type.is_meta_type && !is_constructor && (p_base_type.kind == GDScriptParser::DataType::SCRIPT || p_base_type.kind == GDScriptParser::DataType::CLASS)) { + MethodInfo info; + StringName script_class = p_base_type.kind == GDScriptParser::DataType::SCRIPT ? p_base_type.script_type->get_class_name() : StringName(GDScript::get_class_static()); + + if (ClassDB::get_method_info(script_class, function_name, &info)) { + return function_signature_from_info(info, r_return_type, r_par_types, r_default_arg_count, r_static, r_vararg); + } + } + + StringName base_native = p_base_type.native_type; +#ifdef DEBUG_ENABLED + if (base_native != StringName()) { + // Empty native class might happen in some Script implementations. + // Just ignore it. + if (!class_exists(base_native)) { + ERR_FAIL_V_MSG(false, vformat("Native class %s used in script doesn't exist or isn't exposed.", base_native)); + } + } +#endif + + if (is_constructor) { + // Native types always have a default constructor. + r_return_type = p_base_type; + r_return_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_return_type.is_meta_type = false; + return true; + } + + StringName real_native = get_real_class_name(base_native); + + MethodInfo info; + if (ClassDB::get_method_info(real_native, function_name, &info)) { + return function_signature_from_info(info, r_return_type, r_par_types, r_default_arg_count, r_static, r_vararg); + } + + return false; +} + +bool GDScriptAnalyzer::function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) { + r_return_type = type_from_property(p_info.return_val); + r_default_arg_count = p_info.default_arguments.size(); + r_vararg = (p_info.flags & METHOD_FLAG_VARARG) != 0; + + for (const List<PropertyInfo>::Element *E = p_info.arguments.front(); E != nullptr; E = E->next()) { + r_par_types.push_back(type_from_property(E->get())); + } + return true; +} + +bool GDScriptAnalyzer::validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call) { + List<GDScriptParser::DataType> arg_types; + + for (const List<PropertyInfo>::Element *E = p_method.arguments.front(); E != nullptr; E = E->next()) { + arg_types.push_back(type_from_property(E->get())); + } + + return validate_call_arg(arg_types, p_method.default_arguments.size(), (p_method.flags & METHOD_FLAG_VARARG) != 0, p_call); +} + +bool GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call) { + bool valid = true; + + if (p_call->arguments.size() < p_par_types.size() - p_default_args_count) { + push_error(vformat(R"*(Too few arguments for "%s()" call. Expected at least %d but received %d.)*", p_call->function_name, p_par_types.size() - p_default_args_count, p_call->arguments.size()), p_call); + valid = false; + } + if (!p_is_vararg && p_call->arguments.size() > p_par_types.size()) { + push_error(vformat(R"*(Too many arguments for "%s()" call. Expected at most %d but received %d.)*", p_call->function_name, p_par_types.size(), p_call->arguments.size()), p_call->arguments[p_par_types.size()]); + valid = false; + } + + for (int i = 0; i < p_call->arguments.size(); i++) { + if (i >= p_par_types.size()) { + // Already on vararg place. + break; + } + GDScriptParser::DataType par_type = p_par_types[i]; + GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype(); + + if (arg_type.is_variant()) { + // Argument can be anything, so this is unsafe. + mark_node_unsafe(p_call->arguments[i]); + } else if (!is_type_compatible(par_type, arg_type, true)) { + // Supertypes are acceptable for dynamic compliance, but it's unsafe. + mark_node_unsafe(p_call); + if (!is_type_compatible(arg_type, par_type)) { + push_error(vformat(R"*(Invalid argument for "%s()" function: argument %d should be %s but is %s.)*", + p_call->function_name, i + 1, par_type.to_string(), arg_type.to_string()), + p_call->arguments[i]); + valid = false; + } +#ifdef DEBUG_ENABLED + } else { + if (par_type.kind == GDScriptParser::DataType::BUILTIN && par_type.builtin_type == Variant::INT && arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == Variant::FLOAT) { + parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name); + } +#endif + } + } + return valid; +} + +#ifdef DEBUG_ENABLED +bool GDScriptAnalyzer::is_shadowing(GDScriptParser::IdentifierNode *p_local, const String &p_context) { + const StringName &name = p_local->name; + GDScriptParser::DataType base = parser->current_class->get_datatype(); + + GDScriptParser::ClassNode *base_class = base.class_type; + + while (base_class != nullptr) { + if (base_class->has_member(name)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE, p_context, p_local->name, base_class->get_member(name).get_type_name(), itos(base_class->get_member(name).get_line())); + return true; + } + base_class = base_class->base_type.class_type; + } + + StringName base_native = base.native_type; + + ERR_FAIL_COND_V_MSG(!class_exists(base_native), false, "Non-existent native base class."); + + StringName parent = base_native; + while (parent != StringName()) { + StringName real_class_name = get_real_class_name(parent); + if (ClassDB::has_method(real_class_name, name, true)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE_BASE_CLASS, p_context, p_local->name, "method", parent); + return true; + } else if (ClassDB::has_signal(real_class_name, name, true)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE_BASE_CLASS, p_context, p_local->name, "signal", parent); + return true; + } else if (ClassDB::has_property(real_class_name, name, true)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE_BASE_CLASS, p_context, p_local->name, "property", parent); + return true; + } else if (ClassDB::has_integer_constant(real_class_name, name, true)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE_BASE_CLASS, p_context, p_local->name, "constant", parent); + return true; + } else if (ClassDB::has_enum(real_class_name, name, true)) { + parser->push_warning(p_local, GDScriptWarning::SHADOWED_VARIABLE_BASE_CLASS, p_context, p_local->name, "enum", parent); + return true; + } + parent = ClassDB::get_parent_class(real_class_name); + } + + return false; +} +#endif + +GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, const GDScriptParser::DataType &p_b, bool &r_valid) { + // This function creates dummy variant values and apply the operation to those. Less error-prone than keeping a table of valid operations. + + GDScriptParser::DataType result; + result.kind = GDScriptParser::DataType::VARIANT; + + Variant::Type a_type = p_a.builtin_type; + Variant::Type b_type = p_b.builtin_type; + + Variant a; + REF a_ref; + if (a_type == Variant::OBJECT) { + a_ref.instance(); + a = a_ref; + } else { + Callable::CallError err; + a = Variant::construct(a_type, nullptr, 0, err); + if (err.error != Callable::CallError::CALL_OK) { + r_valid = false; + ERR_FAIL_V_MSG(result, vformat("Could not construct value of type %s", Variant::get_type_name(a_type))); + } + } + Variant b; + REF b_ref; + if (b_type == Variant::OBJECT) { + b_ref.instance(); + b = b_ref; + } else { + Callable::CallError err; + b = Variant::construct(b_type, nullptr, 0, err); + if (err.error != Callable::CallError::CALL_OK) { + r_valid = false; + ERR_FAIL_V_MSG(result, vformat("Could not construct value of type %s", Variant::get_type_name(b_type))); + } + } + + // Avoid division by zero. + switch (b_type) { + case Variant::INT: + b = 1; + break; + case Variant::FLOAT: + b = 1.0; + break; + case Variant::VECTOR2: + b = Vector2(1.0, 1.0); + break; + case Variant::VECTOR2I: + b = Vector2i(1, 1); + break; + case Variant::VECTOR3: + b = Vector3(1.0, 1.0, 1.0); + break; + case Variant::VECTOR3I: + b = Vector3i(1, 1, 1); + break; + case Variant::COLOR: + b = Color(1.0, 1.0, 1.0, 1.0); + break; + default: + // No change needed. + break; + } + + // Avoid error in formatting operator (%) where it doesn't find a placeholder. + if (a_type == Variant::STRING && b_type != Variant::ARRAY) { + a = String("%s"); + } + + Variant ret; + Variant::evaluate(p_operation, a, b, ret, r_valid); + + if (r_valid) { + return type_from_variant(ret); + } + + return result; +} + +// TODO: Add safe/unsafe return variable (for variant cases) +bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion) const { + // These return "true" so it doesn't affect users negatively. + ERR_FAIL_COND_V_MSG(!p_target.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset target type"); + ERR_FAIL_COND_V_MSG(!p_source.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset value type"); + + if (p_target.kind == GDScriptParser::DataType::VARIANT) { + // Variant can receive anything. + return true; + } + + if (p_source.kind == GDScriptParser::DataType::VARIANT) { + // TODO: This is acceptable but unsafe. Make sure unsafe line is set. + return true; + } + + if (p_target.kind == GDScriptParser::DataType::BUILTIN) { + bool valid = p_source.kind == GDScriptParser::DataType::BUILTIN && p_target.builtin_type == p_source.builtin_type; + if (!valid && p_allow_implicit_conversion) { + valid = Variant::can_convert_strict(p_source.builtin_type, p_target.builtin_type); + } + if (!valid && p_target.builtin_type == Variant::INT && p_source.kind == GDScriptParser::DataType::ENUM_VALUE) { + // Enum value is also integer. + valid = true; + } + return valid; + } + + if (p_target.kind == GDScriptParser::DataType::ENUM) { + if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) { + return true; + } + if (p_source.kind == GDScriptParser::DataType::ENUM_VALUE) { + if (p_source.native_type == p_target.native_type && p_target.enum_values.has(p_source.enum_type)) { + return true; + } + } + return false; + } + + // From here on the target type is an object, so we have to test polymorphism. + + if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::NIL) { + // null is acceptable in object. + return true; + } + + StringName src_native; + Ref<Script> src_script; + const GDScriptParser::ClassNode *src_class = nullptr; + + switch (p_source.kind) { + case GDScriptParser::DataType::NATIVE: + if (p_target.kind != GDScriptParser::DataType::NATIVE) { + // Non-native class cannot be supertype of native. + return false; + } + if (p_source.is_meta_type) { + src_native = GDScriptNativeClass::get_class_static(); + } else { + src_native = p_source.native_type; + } + break; + case GDScriptParser::DataType::SCRIPT: + if (p_target.kind == GDScriptParser::DataType::CLASS) { + // A script type cannot be a subtype of a GDScript class. + return false; + } + if (p_source.is_meta_type) { + src_native = p_source.script_type->get_class_name(); + } else { + src_script = p_source.script_type; + src_native = src_script->get_instance_base_type(); + } + break; + case GDScriptParser::DataType::CLASS: + if (p_source.is_meta_type) { + src_native = GDScript::get_class_static(); + } else { + src_class = p_source.class_type; + const GDScriptParser::ClassNode *base = src_class; + while (base->base_type.kind == GDScriptParser::DataType::CLASS) { + base = base->base_type.class_type; + } + src_native = base->base_type.native_type; + src_script = base->base_type.script_type; + } + break; + case GDScriptParser::DataType::VARIANT: + case GDScriptParser::DataType::BUILTIN: + case GDScriptParser::DataType::ENUM: + case GDScriptParser::DataType::ENUM_VALUE: + case GDScriptParser::DataType::UNRESOLVED: + break; // Already solved before. + } + + // Get underscore-prefixed version for some classes. + src_native = get_real_class_name(src_native); + + switch (p_target.kind) { + case GDScriptParser::DataType::NATIVE: { + if (p_target.is_meta_type) { + return ClassDB::is_parent_class(src_native, GDScriptNativeClass::get_class_static()); + } + StringName tgt_native = get_real_class_name(p_target.native_type); + return ClassDB::is_parent_class(src_native, tgt_native); + } + case GDScriptParser::DataType::SCRIPT: + if (p_target.is_meta_type) { + return ClassDB::is_parent_class(src_native, p_target.script_type->get_class_name()); + } + while (src_script.is_valid()) { + if (src_script == p_target.script_type) { + return true; + } + src_script = src_script->get_base_script(); + } + return false; + case GDScriptParser::DataType::CLASS: + if (p_target.is_meta_type) { + return ClassDB::is_parent_class(src_native, GDScript::get_class_static()); + } + while (src_class != nullptr) { + if (src_class->fqcn == p_target.class_type->fqcn) { + return true; + } + src_class = src_class->base_type.class_type; + } + return false; + case GDScriptParser::DataType::VARIANT: + case GDScriptParser::DataType::BUILTIN: + case GDScriptParser::DataType::ENUM: + case GDScriptParser::DataType::ENUM_VALUE: + case GDScriptParser::DataType::UNRESOLVED: + break; // Already solved before. + } + + return false; +} + +void GDScriptAnalyzer::push_error(const String &p_message, const GDScriptParser::Node *p_origin) { + mark_node_unsafe(p_origin); + parser->push_error(p_message, p_origin); +} + +void GDScriptAnalyzer::mark_node_unsafe(const GDScriptParser::Node *p_node) { +#ifdef DEBUG_ENABLED + for (int i = p_node->start_line; i <= p_node->end_line; i++) { + parser->unsafe_lines.insert(i); + } +#endif +} + +bool GDScriptAnalyzer::class_exists(const StringName &p_class) { + StringName real_name = get_real_class_name(p_class); + return ClassDB::class_exists(real_name) && ClassDB::is_class_exposed(real_name); +} + +Ref<GDScriptParserRef> GDScriptAnalyzer::get_parser_for(const String &p_path) { + Ref<GDScriptParserRef> ref; + if (depended_parsers.has(p_path)) { + ref = depended_parsers[p_path]; + } else { + Error err = OK; + ref = GDScriptCache::get_parser(p_path, GDScriptParserRef::EMPTY, err, parser->script_path); + depended_parsers[p_path] = ref; + } + + return ref; +} + +Error GDScriptAnalyzer::resolve_inheritance() { + return resolve_inheritance(parser->head); +} + +Error GDScriptAnalyzer::resolve_interface() { + resolve_class_interface(parser->head); + return parser->errors.empty() ? OK : ERR_PARSE_ERROR; +} + +Error GDScriptAnalyzer::resolve_body() { + resolve_class_body(parser->head); + return parser->errors.empty() ? OK : ERR_PARSE_ERROR; +} + +Error GDScriptAnalyzer::resolve_program() { + resolve_class_interface(parser->head); + resolve_class_body(parser->head); + + List<String> parser_keys; + depended_parsers.get_key_list(&parser_keys); + for (const List<String>::Element *E = parser_keys.front(); E != nullptr; E = E->next()) { + depended_parsers[E->get()]->raise_status(GDScriptParserRef::FULLY_SOLVED); + } + depended_parsers.clear(); + return parser->errors.empty() ? OK : ERR_PARSE_ERROR; +} + +Error GDScriptAnalyzer::analyze() { + parser->errors.clear(); + Error err = resolve_inheritance(parser->head); + if (err) { + return err; + } + return resolve_program(); +} + +GDScriptAnalyzer::GDScriptAnalyzer(GDScriptParser *p_parser) { + parser = p_parser; +} diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h new file mode 100644 index 0000000000..da767522ad --- /dev/null +++ b/modules/gdscript/gdscript_analyzer.h @@ -0,0 +1,122 @@ +/*************************************************************************/ +/* gdscript_analyzer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef GDSCRIPT_ANALYZER_H +#define GDSCRIPT_ANALYZER_H + +#include "core/object.h" +#include "core/reference.h" +#include "core/set.h" +#include "gdscript_cache.h" +#include "gdscript_parser.h" + +class GDScriptAnalyzer { + GDScriptParser *parser = nullptr; + HashMap<String, Ref<GDScriptParserRef>> depended_parsers; + + const GDScriptParser::EnumNode *current_enum = nullptr; + + Error resolve_inheritance(GDScriptParser::ClassNode *p_class, bool p_recursive = true); + GDScriptParser::DataType resolve_datatype(GDScriptParser::TypeNode *p_type); + + void decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement); + + // This traverses the tree to resolve all TypeNodes. + Error resolve_program(); + + void resolve_annotation(GDScriptParser::AnnotationNode *p_annotation); + void resolve_class_interface(GDScriptParser::ClassNode *p_class); + void resolve_class_body(GDScriptParser::ClassNode *p_class); + void resolve_function_signature(GDScriptParser::FunctionNode *p_function); + void resolve_function_body(GDScriptParser::FunctionNode *p_function); + void resolve_node(GDScriptParser::Node *p_node); + void resolve_suite(GDScriptParser::SuiteNode *p_suite); + void resolve_if(GDScriptParser::IfNode *p_if); + void resolve_for(GDScriptParser::ForNode *p_for); + void resolve_while(GDScriptParser::WhileNode *p_while); + void resolve_variable(GDScriptParser::VariableNode *p_variable); + void resolve_constant(GDScriptParser::ConstantNode *p_constant); + void resolve_assert(GDScriptParser::AssertNode *p_assert); + void resolve_match(GDScriptParser::MatchNode *p_match); + void resolve_match_branch(GDScriptParser::MatchBranchNode *p_match_branch, GDScriptParser::ExpressionNode *p_match_test); + void resolve_match_pattern(GDScriptParser::PatternNode *p_match_pattern, GDScriptParser::ExpressionNode *p_match_test); + void resolve_pararameter(GDScriptParser::ParameterNode *p_parameter); + void resolve_return(GDScriptParser::ReturnNode *p_return); + + // Reduction functions. + void reduce_expression(GDScriptParser::ExpressionNode *p_expression); + void reduce_array(GDScriptParser::ArrayNode *p_array); + void reduce_assignment(GDScriptParser::AssignmentNode *p_assignment); + void reduce_await(GDScriptParser::AwaitNode *p_await); + void reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_op); + void reduce_call(GDScriptParser::CallNode *p_call, bool is_await = false); + void reduce_cast(GDScriptParser::CastNode *p_cast); + void reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary); + void reduce_get_node(GDScriptParser::GetNodeNode *p_get_node); + void reduce_identifier(GDScriptParser::IdentifierNode *p_identifier, bool can_be_builtin = false); + void reduce_identifier_from_base(GDScriptParser::IdentifierNode *p_identifier, GDScriptParser::DataType *p_base = nullptr); + void reduce_literal(GDScriptParser::LiteralNode *p_literal); + void reduce_preload(GDScriptParser::PreloadNode *p_preload); + void reduce_self(GDScriptParser::SelfNode *p_self); + void reduce_subscript(GDScriptParser::SubscriptNode *p_subscript); + void reduce_ternary_op(GDScriptParser::TernaryOpNode *p_ternary_op); + void reduce_unary_op(GDScriptParser::UnaryOpNode *p_unary_op); + + // Helpers. + GDScriptParser::DataType type_from_variant(const Variant &p_value); + GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type) const; + GDScriptParser::DataType type_from_property(const PropertyInfo &p_property) const; + GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name); + bool get_function_signature(GDScriptParser::Node *p_source, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); + bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); + bool validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call); + bool validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call); + GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, const GDScriptParser::DataType &p_b, bool &r_valid); + bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false) const; + void push_error(const String &p_message, const GDScriptParser::Node *p_origin); + void mark_node_unsafe(const GDScriptParser::Node *p_node); + bool class_exists(const StringName &p_class); + Ref<GDScriptParserRef> get_parser_for(const String &p_path); +#ifdef DEBUG_ENABLED + bool is_shadowing(GDScriptParser::IdentifierNode *p_local, const String &p_context); +#endif + +public: + Error resolve_inheritance(); + Error resolve_interface(); + Error resolve_body(); + Error analyze(); + + GDScriptAnalyzer(GDScriptParser *p_parser); + + static void cleanup(); +}; + +#endif // GDSCRIPT_ANALYZER_H diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp new file mode 100644 index 0000000000..cdb14d6281 --- /dev/null +++ b/modules/gdscript/gdscript_cache.cpp @@ -0,0 +1,259 @@ +/*************************************************************************/ +/* gdscript_cache.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#include "gdscript_cache.h" + +#include "core/os/file_access.h" +#include "core/vector.h" +#include "gdscript.h" +#include "gdscript_analyzer.h" +#include "gdscript_parser.h" + +bool GDScriptParserRef::is_valid() const { + return parser != nullptr; +} + +GDScriptParserRef::Status GDScriptParserRef::get_status() const { + return status; +} + +GDScriptParser *GDScriptParserRef::get_parser() const { + return parser; +} + +Error GDScriptParserRef::raise_status(Status p_new_status) { + ERR_FAIL_COND_V(parser == nullptr, ERR_INVALID_DATA); + + Error result = OK; + + while (p_new_status > status) { + switch (status) { + case EMPTY: + result = parser->parse(GDScriptCache::get_source_code(path), path, false); + status = PARSED; + break; + case PARSED: { + analyzer = memnew(GDScriptAnalyzer(parser)); + Error inheritance_result = analyzer->resolve_inheritance(); + status = INHERITANCE_SOLVED; + if (result == OK) { + result = inheritance_result; + } + } break; + case INHERITANCE_SOLVED: { + Error interface_result = analyzer->resolve_interface(); + status = INTERFACE_SOLVED; + if (result == OK) { + result = interface_result; + } + } break; + case INTERFACE_SOLVED: { + Error body_result = analyzer->resolve_body(); + status = FULLY_SOLVED; + if (result == OK) { + result = body_result; + } + } break; + case FULLY_SOLVED: { + return result; + } + } + if (result != OK) { + if (parser != nullptr) { + memdelete(parser); + parser = nullptr; + } + if (analyzer != nullptr) { + memdelete(analyzer); + analyzer = nullptr; + } + return result; + } + } + + return result; +} + +GDScriptParserRef::~GDScriptParserRef() { + if (parser != nullptr) { + memdelete(parser); + } + if (analyzer != nullptr) { + memdelete(analyzer); + } + MutexLock(GDScriptCache::singleton->lock); + GDScriptCache::singleton->parser_map.erase(path); +} + +GDScriptCache *GDScriptCache::singleton = nullptr; + +void GDScriptCache::remove_script(const String &p_path) { + MutexLock(singleton->lock); + singleton->shallow_gdscript_cache.erase(p_path); + singleton->full_gdscript_cache.erase(p_path); +} + +Ref<GDScriptParserRef> GDScriptCache::get_parser(const String &p_path, GDScriptParserRef::Status p_status, Error &r_error, const String &p_owner) { + MutexLock(singleton->lock); + Ref<GDScriptParserRef> ref; + if (p_owner != String()) { + singleton->dependencies[p_owner].insert(p_path); + } + if (singleton->parser_map.has(p_path)) { + ref = singleton->parser_map[p_path]; + } else { + if (!FileAccess::exists(p_path)) { + r_error = ERR_FILE_NOT_FOUND; + return ref; + } + GDScriptParser *parser = memnew(GDScriptParser); + ref.instance(); + ref->parser = parser; + ref->path = p_path; + singleton->parser_map[p_path] = ref; + ref->unreference(); + } + + r_error = ref->raise_status(p_status); + + return ref; +} + +String GDScriptCache::get_source_code(const String &p_path) { + Vector<uint8_t> source_file; + Error err; + FileAccessRef f = FileAccess::open(p_path, FileAccess::READ, &err); + if (err) { + ERR_FAIL_COND_V(err, ""); + } + + int len = f->get_len(); + source_file.resize(len + 1); + int r = f->get_buffer(source_file.ptrw(), len); + f->close(); + ERR_FAIL_COND_V(r != len, ""); + source_file.write[len] = 0; + + String source; + if (source.parse_utf8((const char *)source_file.ptr())) { + ERR_FAIL_V_MSG("", "Script '" + p_path + "' contains invalid unicode (UTF-8), so it was not loaded. Please ensure that scripts are saved in valid UTF-8 unicode."); + } + return source; +} + +Ref<GDScript> GDScriptCache::get_shallow_script(const String &p_path, const String &p_owner) { + MutexLock(singleton->lock); + if (p_owner != String()) { + singleton->dependencies[p_owner].insert(p_path); + } + if (singleton->full_gdscript_cache.has(p_path)) { + return singleton->full_gdscript_cache[p_path]; + } + if (singleton->shallow_gdscript_cache.has(p_path)) { + return singleton->shallow_gdscript_cache[p_path]; + } + + Ref<GDScript> script; + script.instance(); + script->set_path(p_path, true); + script->set_script_path(p_path); + script->load_source_code(p_path); + + singleton->shallow_gdscript_cache[p_path] = script; + // The one in cache is not a hard reference: if the script dies somewhere else it's fine. + // Scripts remove themselves from cache when they die. + script->unreference(); + return script; +} + +Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_error, const String &p_owner) { + MutexLock(singleton->lock); + + if (p_owner != String()) { + singleton->dependencies[p_owner].insert(p_path); + } + + r_error = OK; + if (singleton->full_gdscript_cache.has(p_path)) { + return singleton->full_gdscript_cache[p_path]; + } + Ref<GDScript> script = get_shallow_script(p_path); + + r_error = script->load_source_code(p_path); + + if (r_error) { + return script; + } + + r_error = script->reload(); + if (r_error) { + return script; + } + + singleton->full_gdscript_cache[p_path] = script; + singleton->shallow_gdscript_cache.erase(p_path); + + return script; +} + +Error GDScriptCache::finish_compiling(const String &p_owner) { + // Mark this as compiled. + Ref<GDScript> script = get_shallow_script(p_owner); + singleton->full_gdscript_cache[p_owner] = script; + singleton->shallow_gdscript_cache.erase(p_owner); + + Set<String> depends = singleton->dependencies[p_owner]; + + Error err = OK; + for (const Set<String>::Element *E = depends.front(); E != nullptr; E = E->next()) { + Error this_err = OK; + // No need to save the script. We assume it's already referenced in the owner. + get_full_script(E->get(), this_err); + + if (this_err != OK) { + err = this_err; + } + } + + singleton->dependencies.erase(p_owner); + + return err; +} + +GDScriptCache::GDScriptCache() { + singleton = this; +} + +GDScriptCache::~GDScriptCache() { + parser_map.clear(); + shallow_gdscript_cache.clear(); + full_gdscript_cache.clear(); + singleton = nullptr; +} diff --git a/modules/mono/editor/csharp_project.cpp b/modules/gdscript/gdscript_cache.h index 6f54eb09a2..770704d6eb 100644 --- a/modules/mono/editor/csharp_project.cpp +++ b/modules/gdscript/gdscript_cache.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* csharp_project.cpp */ +/* gdscript_cache.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,42 +28,70 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "csharp_project.h" +#ifndef GDSCRIPT_CACHE_H +#define GDSCRIPT_CACHE_H -#include "core/io/json.h" -#include "core/os/dir_access.h" -#include "core/os/file_access.h" -#include "core/os/os.h" -#include "core/project_settings.h" +#include "core/hash_map.h" +#include "core/os/mutex.h" +#include "core/reference.h" +#include "core/set.h" +#include "gdscript.h" -#include "../csharp_script.h" -#include "../mono_gd/gd_mono_class.h" -#include "../mono_gd/gd_mono_marshal.h" -#include "../utils/string_utils.h" -#include "script_class_parser.h" +class GDScriptAnalyzer; +class GDScriptParser; -namespace CSharpProject { +class GDScriptParserRef : public Reference { +public: + enum Status { + EMPTY, + PARSED, + INHERITANCE_SOLVED, + INTERFACE_SOLVED, + FULLY_SOLVED, + }; -void add_item(const String &p_project_path, const String &p_item_type, const String &p_include) { - if (!GLOBAL_DEF("mono/project/auto_update_project", true)) { - return; - } +private: + GDScriptParser *parser = nullptr; + GDScriptAnalyzer *analyzer = nullptr; + Status status = EMPTY; + String path; - GDMonoAssembly *tools_project_editor_assembly = GDMono::get_singleton()->get_tools_project_editor_assembly(); + friend class GDScriptCache; - GDMonoClass *klass = tools_project_editor_assembly->get_class("GodotTools.ProjectEditor", "ProjectUtils"); +public: + bool is_valid() const; + Status get_status() const; + GDScriptParser *get_parser() const; + Error raise_status(Status p_new_status); - Variant project_path = p_project_path; - Variant item_type = p_item_type; - Variant include = p_include; - const Variant *args[3] = { &project_path, &item_type, &include }; - MonoException *exc = nullptr; - klass->get_method("AddItemToProjectChecked", 3)->invoke(nullptr, args, &exc); + GDScriptParserRef() {} + ~GDScriptParserRef(); +}; - if (exc) { - GDMonoUtils::debug_print_unhandled_exception(exc); - ERR_FAIL(); - } -} +class GDScriptCache { + // String key is full path. + HashMap<String, Ref<GDScriptParserRef>> parser_map; + HashMap<String, Ref<GDScript>> shallow_gdscript_cache; + HashMap<String, Ref<GDScript>> full_gdscript_cache; + HashMap<String, Set<String>> dependencies; -} // namespace CSharpProject + friend class GDScript; + friend class GDScriptParserRef; + + static GDScriptCache *singleton; + + Mutex lock; + static void remove_script(const String &p_path); + +public: + static Ref<GDScriptParserRef> get_parser(const String &p_path, GDScriptParserRef::Status status, Error &r_error, const String &p_owner = String()); + static String get_source_code(const String &p_path); + static Ref<GDScript> get_shallow_script(const String &p_path, const String &p_owner = String()); + static Ref<GDScript> get_full_script(const String &p_path, Error &r_error, const String &p_owner = String()); + static Error finish_compiling(const String &p_owner); + + GDScriptCache(); + ~GDScriptCache(); +}; + +#endif // GDSCRIPT_CACHE_H diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 5bc9003c29..e34d87f5cc 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -31,9 +31,10 @@ #include "gdscript_compiler.h" #include "gdscript.h" +#include "gdscript_cache.h" bool GDScriptCompiler::_is_class_member_property(CodeGen &codegen, const StringName &p_name) { - if (codegen.function_node && codegen.function_node->_static) { + if (codegen.function_node && codegen.function_node->is_static) { return false; } @@ -66,18 +67,16 @@ void GDScriptCompiler::_set_error(const String &p_error, const GDScriptParser::N error = p_error; if (p_node) { - err_line = p_node->line; - err_column = p_node->column; + err_line = p_node->start_line; + err_column = p_node->leftmost_column; } else { err_line = 0; err_column = 0; } } -bool GDScriptCompiler::_create_unary_operator(CodeGen &codegen, const GDScriptParser::OperatorNode *on, Variant::Operator op, int p_stack_level) { - ERR_FAIL_COND_V(on->arguments.size() != 1, false); - - int src_address_a = _parse_expression(codegen, on->arguments[0], p_stack_level); +bool GDScriptCompiler::_create_unary_operator(CodeGen &codegen, const GDScriptParser::UnaryOpNode *on, Variant::Operator op, int p_stack_level) { + int src_address_a = _parse_expression(codegen, on->operand, p_stack_level); if (src_address_a < 0) { return false; } @@ -90,10 +89,8 @@ bool GDScriptCompiler::_create_unary_operator(CodeGen &codegen, const GDScriptPa return true; } -bool GDScriptCompiler::_create_binary_operator(CodeGen &codegen, const GDScriptParser::OperatorNode *on, Variant::Operator op, int p_stack_level, bool p_initializer, int p_index_addr) { - ERR_FAIL_COND_V(on->arguments.size() != 2, false); - - int src_address_a = _parse_expression(codegen, on->arguments[0], p_stack_level, false, p_initializer, p_index_addr); +bool GDScriptCompiler::_create_binary_operator(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_left_operand, const GDScriptParser::ExpressionNode *p_right_operand, Variant::Operator op, int p_stack_level, bool p_initializer, int p_index_addr) { + int src_address_a = _parse_expression(codegen, p_left_operand, p_stack_level, false, p_initializer, p_index_addr); if (src_address_a < 0) { return false; } @@ -101,7 +98,7 @@ bool GDScriptCompiler::_create_binary_operator(CodeGen &codegen, const GDScriptP p_stack_level++; //uses stack for return, increase stack } - int src_address_b = _parse_expression(codegen, on->arguments[1], p_stack_level, false, p_initializer); + int src_address_b = _parse_expression(codegen, p_right_operand, p_stack_level, false, p_initializer); if (src_address_b < 0) { return false; } @@ -113,8 +110,12 @@ bool GDScriptCompiler::_create_binary_operator(CodeGen &codegen, const GDScriptP return true; } +bool GDScriptCompiler::_create_binary_operator(CodeGen &codegen, const GDScriptParser::BinaryOpNode *on, Variant::Operator op, int p_stack_level, bool p_initializer, int p_index_addr) { + return _create_binary_operator(codegen, on->left_operand, on->right_operand, op, p_stack_level, p_initializer, p_index_addr); +} + GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::DataType &p_datatype) const { - if (!p_datatype.has_type) { + if (!p_datatype.is_set() || !p_datatype.is_hard_type()) { return GDScriptDataType(); } @@ -122,6 +123,9 @@ GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::D result.has_type = true; switch (p_datatype.kind) { + case GDScriptParser::DataType::VARIANT: { + result.has_type = false; + } break; case GDScriptParser::DataType::BUILTIN: { result.kind = GDScriptDataType::BUILTIN; result.builtin_type = p_datatype.builtin_type; @@ -135,36 +139,49 @@ GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::D result.script_type = p_datatype.script_type; result.native_type = result.script_type->get_instance_base_type(); } break; - case GDScriptParser::DataType::GDSCRIPT: { - result.kind = GDScriptDataType::GDSCRIPT; - result.script_type = p_datatype.script_type; - result.native_type = result.script_type->get_instance_base_type(); - } break; case GDScriptParser::DataType::CLASS: { // Locate class by constructing the path to it and following that path GDScriptParser::ClassNode *class_type = p_datatype.class_type; - List<StringName> names; - while (class_type->owner) { - names.push_back(class_type->name); - class_type = class_type->owner; - } + if (class_type) { + if (class_type->fqcn.begins_with(main_script->path) || (!main_script->name.empty() && class_type->fqcn.begins_with(main_script->name))) { + // Local class. + List<StringName> names; + while (class_type->outer) { + names.push_back(class_type->identifier->name); + class_type = class_type->outer; + } - Ref<GDScript> script = Ref<GDScript>(main_script); - while (names.back()) { - if (!script->subclasses.has(names.back()->get())) { - ERR_PRINT("Parser bug: Cannot locate datatype class."); - result.has_type = false; - return GDScriptDataType(); + Ref<GDScript> script = Ref<GDScript>(main_script); + while (names.back()) { + if (!script->subclasses.has(names.back()->get())) { + ERR_PRINT("Parser bug: Cannot locate datatype class."); + result.has_type = false; + return GDScriptDataType(); + } + script = script->subclasses[names.back()->get()]; + names.pop_back(); + } + result.kind = GDScriptDataType::GDSCRIPT; + result.script_type = script; + result.native_type = script->get_instance_base_type(); + } else { + result.kind = GDScriptDataType::GDSCRIPT; + result.script_type = GDScriptCache::get_shallow_script(p_datatype.script_path, main_script->path); + result.native_type = p_datatype.native_type; } - script = script->subclasses[names.back()->get()]; - names.pop_back(); } - - result.kind = GDScriptDataType::GDSCRIPT; - result.script_type = script; - result.native_type = script->get_instance_base_type(); } break; - default: { + case GDScriptParser::DataType::ENUM_VALUE: + result.has_type = true; + result.kind = GDScriptDataType::BUILTIN; + result.builtin_type = Variant::INT; + break; + case GDScriptParser::DataType::ENUM: + result.has_type = true; + result.kind = GDScriptDataType::BUILTIN; + result.builtin_type = Variant::DICTIONARY; + break; + case GDScriptParser::DataType::UNRESOLVED: { ERR_PRINT("Parser bug: converting unresolved type."); return GDScriptDataType(); } @@ -173,42 +190,41 @@ GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::D return result; } -int GDScriptCompiler::_parse_assign_right_expression(CodeGen &codegen, const GDScriptParser::OperatorNode *p_expression, int p_stack_level, int p_index_addr) { +int GDScriptCompiler::_parse_assign_right_expression(CodeGen &codegen, const GDScriptParser::AssignmentNode *p_assignment, int p_stack_level, int p_index_addr) { Variant::Operator var_op = Variant::OP_MAX; - switch (p_expression->op) { - case GDScriptParser::OperatorNode::OP_ASSIGN_ADD: + switch (p_assignment->operation) { + case GDScriptParser::AssignmentNode::OP_ADDITION: var_op = Variant::OP_ADD; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_SUB: + case GDScriptParser::AssignmentNode::OP_SUBTRACTION: var_op = Variant::OP_SUBTRACT; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_MUL: + case GDScriptParser::AssignmentNode::OP_MULTIPLICATION: var_op = Variant::OP_MULTIPLY; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_DIV: + case GDScriptParser::AssignmentNode::OP_DIVISION: var_op = Variant::OP_DIVIDE; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_MOD: + case GDScriptParser::AssignmentNode::OP_MODULO: var_op = Variant::OP_MODULE; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_SHIFT_LEFT: + case GDScriptParser::AssignmentNode::OP_BIT_SHIFT_LEFT: var_op = Variant::OP_SHIFT_LEFT; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_SHIFT_RIGHT: + case GDScriptParser::AssignmentNode::OP_BIT_SHIFT_RIGHT: var_op = Variant::OP_SHIFT_RIGHT; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_AND: + case GDScriptParser::AssignmentNode::OP_BIT_AND: var_op = Variant::OP_BIT_AND; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_OR: + case GDScriptParser::AssignmentNode::OP_BIT_OR: var_op = Variant::OP_BIT_OR; break; - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_XOR: + case GDScriptParser::AssignmentNode::OP_BIT_XOR: var_op = Variant::OP_BIT_XOR; break; - case GDScriptParser::OperatorNode::OP_INIT_ASSIGN: - case GDScriptParser::OperatorNode::OP_ASSIGN: { + case GDScriptParser::AssignmentNode::OP_NONE: { //none } break; default: { @@ -216,13 +232,13 @@ int GDScriptCompiler::_parse_assign_right_expression(CodeGen &codegen, const GDS } } - bool initializer = p_expression->op == GDScriptParser::OperatorNode::OP_INIT_ASSIGN; + // bool initializer = p_expression->op == GDScriptParser::OperatorNode::OP_INIT_ASSIGN; if (var_op == Variant::OP_MAX) { - return _parse_expression(codegen, p_expression->arguments[1], p_stack_level, false, initializer); + return _parse_expression(codegen, p_assignment->assigned_value, p_stack_level, false, false); } - if (!_create_binary_operator(codegen, p_expression, var_op, p_stack_level, initializer, p_index_addr)) { + if (!_create_binary_operator(codegen, p_assignment->assignee, p_assignment->assigned_value, var_op, p_stack_level, false, p_index_addr)) { return -1; } @@ -232,10 +248,74 @@ int GDScriptCompiler::_parse_assign_right_expression(CodeGen &codegen, const GDS return dst_addr; } -int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser::Node *p_expression, int p_stack_level, bool p_root, bool p_initializer, int p_index_addr) { +bool GDScriptCompiler::_generate_typed_assign(CodeGen &codegen, int p_src_address, int p_dst_address, const GDScriptDataType &p_datatype, const GDScriptParser::DataType &p_value_type) { + if (p_datatype.has_type && p_value_type.is_variant()) { + // Typed assignment + switch (p_datatype.kind) { + case GDScriptDataType::BUILTIN: { + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_BUILTIN); // perform operator + codegen.opcodes.push_back(p_datatype.builtin_type); // variable type + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 + } break; + case GDScriptDataType::NATIVE: { + int class_idx; + if (GDScriptLanguage::get_singleton()->get_global_map().has(p_datatype.native_type)) { + class_idx = GDScriptLanguage::get_singleton()->get_global_map()[p_datatype.native_type]; + class_idx |= (GDScriptFunction::ADDR_TYPE_GLOBAL << GDScriptFunction::ADDR_BITS); //argument (stack root) + } else { + // _set_error("Invalid native class type '" + String(p_datatype.native_type) + "'.", on->arguments[0]); + return false; + } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_NATIVE); // perform operator + codegen.opcodes.push_back(class_idx); // variable type + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 + } break; + case GDScriptDataType::SCRIPT: + case GDScriptDataType::GDSCRIPT: { + Variant script = p_datatype.script_type; + int idx = codegen.get_constant_pos(script); //make it a local constant (faster access) + + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_SCRIPT); // perform operator + codegen.opcodes.push_back(idx); // variable type + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 + } break; + default: { + ERR_PRINT("Compiler bug: unresolved assign."); + + // Shouldn't get here, but fail-safe to a regular assignment + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); // perform operator + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 (unary only takes one parameter) + } + } + } else { + if (p_datatype.kind == GDScriptDataType::BUILTIN && p_value_type.kind == GDScriptParser::DataType::BUILTIN && p_datatype.builtin_type != p_value_type.builtin_type) { + // Need conversion. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_BUILTIN); // perform operator + codegen.opcodes.push_back(p_datatype.builtin_type); // variable type + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 + } else { + // Either untyped assignment or already type-checked by the parser + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); // perform operator + codegen.opcodes.push_back(p_dst_address); // argument 1 + codegen.opcodes.push_back(p_src_address); // argument 2 (unary only takes one parameter) + } + } + return true; +} + +int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_expression, int p_stack_level, bool p_root, bool p_initializer, int p_index_addr) { + if (p_expression->is_constant) { + return codegen.get_constant_pos(p_expression->reduced_value); + } + switch (p_expression->type) { //should parse variable declaration and adjust stack accordingly... - case GDScriptParser::Node::TYPE_IDENTIFIER: { + case GDScriptParser::Node::IDENTIFIER: { //return identifier //wait, identifier could be a local variable or something else... careful here, must reference properly //as stack may be more interesting to work with @@ -252,6 +332,11 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return pos | (GDScriptFunction::ADDR_TYPE_STACK_VARIABLE << GDScriptFunction::ADDR_BITS); } + // TRY LOCAL CONSTANTS! + if (codegen.local_named_constants.has(identifier)) { + return codegen.local_named_constants[identifier] | (GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS); + } + // TRY CLASS MEMBER if (_is_class_member_property(codegen, identifier)) { //get property @@ -264,12 +349,26 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: } //TRY MEMBERS! - if (!codegen.function_node || !codegen.function_node->_static) { + if (!codegen.function_node || !codegen.function_node->is_static) { // TRY MEMBER VARIABLES! //static function if (codegen.script->member_indices.has(identifier)) { - int idx = codegen.script->member_indices[identifier].index; - return idx | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS); //argument (stack root) + if (codegen.script->member_indices[identifier].getter != StringName() && codegen.script->member_indices[identifier].getter != codegen.function_name) { + // Perform getter. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_RETURN); + codegen.opcodes.push_back(0); // Argument count. + codegen.opcodes.push_back(GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); // Base (self). + codegen.opcodes.push_back(codegen.get_name_map_pos(codegen.script->member_indices[identifier].getter)); // Method name. + // Destination. + int dst_addr = (p_stack_level) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + codegen.opcodes.push_back(dst_addr); // append the stack level as destination address of the opcode + codegen.alloc_stack(p_stack_level); + return dst_addr; + } else { + // No getter or inside getter: direct member access. + int idx = codegen.script->member_indices[identifier].index; + return idx | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS); //argument (stack root) + } } } @@ -315,6 +414,21 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: owner = owner->_owner; } + // TRY SIGNALS AND METHODS (can be made callables) + if (codegen.class_node->members_indices.has(identifier)) { + const GDScriptParser::ClassNode::Member &member = codegen.class_node->members[codegen.class_node->members_indices[identifier]]; + if (member.type == GDScriptParser::ClassNode::Member::FUNCTION || member.type == GDScriptParser::ClassNode::Member::SIGNAL) { + // Get like it was a property. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_GET_NAMED); // perform operator + codegen.opcodes.push_back(GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); // Self. + codegen.opcodes.push_back(codegen.get_name_map_pos(identifier)); // argument 2 (unary only takes one parameter) + int dst_addr = (p_stack_level) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + codegen.opcodes.push_back(dst_addr); // append the stack level as destination address of the opcode + codegen.alloc_stack(p_stack_level); + return dst_addr; + } + } + if (GDScriptLanguage::get_singleton()->get_global_map().has(identifier)) { int idx = GDScriptLanguage::get_singleton()->get_global_map()[identifier]; return idx | (GDScriptFunction::ADDR_TYPE_GLOBAL << GDScriptFunction::ADDR_BITS); //argument (stack root) @@ -324,19 +438,20 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: if (ScriptServer::is_global_class(identifier)) { const GDScriptParser::ClassNode *class_node = codegen.class_node; - while (class_node->owner) { - class_node = class_node->owner; + while (class_node->outer) { + class_node = class_node->outer; } - if (class_node->name == identifier) { - _set_error("Using own name in class file is not allowed (creates a cyclic reference)", p_expression); - return -1; - } + RES res; - RES res = ResourceLoader::load(ScriptServer::get_global_class_path(identifier)); - if (res.is_null()) { - _set_error("Can't load global class " + String(identifier) + ", cyclic reference?", p_expression); - return -1; + if (class_node->identifier && class_node->identifier->name == identifier) { + res = Ref<GDScript>(main_script); + } else { + res = ResourceLoader::load(ScriptServer::get_global_class_path(identifier)); + if (res.is_null()) { + _set_error("Can't load global class " + String(identifier) + ", cyclic reference?", p_expression); + return -1; + } } Variant key = res; @@ -371,9 +486,9 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return -1; } break; - case GDScriptParser::Node::TYPE_CONSTANT: { + case GDScriptParser::Node::LITERAL: { //return constant - const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(p_expression); + const GDScriptParser::LiteralNode *cn = static_cast<const GDScriptParser::LiteralNode *>(p_expression); int idx; @@ -388,15 +503,15 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return idx | (GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS); //argument (stack root) } break; - case GDScriptParser::Node::TYPE_SELF: { + case GDScriptParser::Node::SELF: { //return constant - if (codegen.function_node && codegen.function_node->_static) { + if (codegen.function_node && codegen.function_node->is_static) { _set_error("'self' not present in static function!", p_expression); return -1; } return (GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); } break; - case GDScriptParser::Node::TYPE_ARRAY: { + case GDScriptParser::Node::ARRAY: { const GDScriptParser::ArrayNode *an = static_cast<const GDScriptParser::ArrayNode *>(p_expression); Vector<int> values; @@ -427,23 +542,35 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return dst_addr; } break; - case GDScriptParser::Node::TYPE_DICTIONARY: { + case GDScriptParser::Node::DICTIONARY: { const GDScriptParser::DictionaryNode *dn = static_cast<const GDScriptParser::DictionaryNode *>(p_expression); - Vector<int> values; + Vector<int> elements; int slevel = p_stack_level; for (int i = 0; i < dn->elements.size(); i++) { - int ret = _parse_expression(codegen, dn->elements[i].key, slevel); - if (ret < 0) { - return ret; - } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { - slevel++; - codegen.alloc_stack(slevel); + // Key. + int ret = -1; + switch (dn->style) { + case GDScriptParser::DictionaryNode::PYTHON_DICT: + // Python-style: key is any expression. + ret = _parse_expression(codegen, dn->elements[i].key, slevel); + if (ret < 0) { + return ret; + } + if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); + } + break; + case GDScriptParser::DictionaryNode::LUA_TABLE: + // Lua-style: key is an identifier interpreted as string. + String key = static_cast<const GDScriptParser::IdentifierNode *>(dn->elements[i].key)->name; + ret = codegen.get_constant_pos(key); + break; } - values.push_back(ret); + elements.push_back(ret); ret = _parse_expression(codegen, dn->elements[i].value, slevel); if (ret < 0) { @@ -454,13 +581,13 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: codegen.alloc_stack(slevel); } - values.push_back(ret); + elements.push_back(ret); } codegen.opcodes.push_back(GDScriptFunction::OPCODE_CONSTRUCT_DICTIONARY); codegen.opcodes.push_back(dn->elements.size()); - for (int i = 0; i < values.size(); i++) { - codegen.opcodes.push_back(values[i]); + for (int i = 0; i < elements.size(); i++) { + codegen.opcodes.push_back(elements[i]); } int dst_addr = (p_stack_level) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); @@ -469,11 +596,11 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return dst_addr; } break; - case GDScriptParser::Node::TYPE_CAST: { + case GDScriptParser::Node::CAST: { const GDScriptParser::CastNode *cn = static_cast<const GDScriptParser::CastNode *>(p_expression); int slevel = p_stack_level; - int src_addr = _parse_expression(codegen, cn->source_node, slevel); + int src_addr = _parse_expression(codegen, cn->operand, slevel); if (src_addr < 0) { return src_addr; } @@ -482,7 +609,7 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: codegen.alloc_stack(slevel); } - GDScriptDataType cast_type = _gdtype_from_datatype(cn->cast_type); + GDScriptDataType cast_type = _gdtype_from_datatype(cn->cast_type->get_datatype()); switch (cast_type.kind) { case GDScriptDataType::BUILTIN: { @@ -504,8 +631,7 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: case GDScriptDataType::SCRIPT: case GDScriptDataType::GDSCRIPT: { Variant script = cast_type.script_type; - int idx = codegen.get_constant_pos(script); - idx |= GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS; //make it a local constant (faster access) + int idx = codegen.get_constant_pos(script); //make it a local constant (faster access) codegen.opcodes.push_back(GDScriptFunction::OPCODE_CAST_TO_SCRIPT); // perform operator codegen.opcodes.push_back(idx); // variable type @@ -523,244 +649,310 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return dst_addr; } break; - case GDScriptParser::Node::TYPE_OPERATOR: { - //hell breaks loose + //hell breaks loose + +#define OPERATOR_RETURN \ + int dst_addr = (p_stack_level) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); \ + codegen.opcodes.push_back(dst_addr); \ + codegen.alloc_stack(p_stack_level); \ + return dst_addr + + case GDScriptParser::Node::CALL: { + const GDScriptParser::CallNode *call = static_cast<const GDScriptParser::CallNode *>(p_expression); + if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name) != Variant::VARIANT_MAX) { + //construct a basic type + + Variant::Type vtype = GDScriptParser::get_builtin_type(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name); + + Vector<int> arguments; + int slevel = p_stack_level; + for (int i = 0; i < call->arguments.size(); i++) { + int ret = _parse_expression(codegen, call->arguments[i], slevel); + if (ret < 0) { + return ret; + } + if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); + } + arguments.push_back(ret); + } - const GDScriptParser::OperatorNode *on = static_cast<const GDScriptParser::OperatorNode *>(p_expression); - switch (on->op) { - //call/constructor operator - case GDScriptParser::OperatorNode::OP_PARENT_CALL: { - ERR_FAIL_COND_V(on->arguments.size() < 1, -1); + //push call bytecode + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CONSTRUCT); // basic type constructor + codegen.opcodes.push_back(vtype); //instance + codegen.opcodes.push_back(arguments.size()); //argument count + codegen.alloc_call(arguments.size()); + for (int i = 0; i < arguments.size(); i++) { + codegen.opcodes.push_back(arguments[i]); //arguments + } - const GDScriptParser::IdentifierNode *in = (const GDScriptParser::IdentifierNode *)on->arguments[0]; + } else if (!call->is_super && call->callee->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_function(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name) != GDScriptFunctions::FUNC_MAX) { + //built in function - Vector<int> arguments; - int slevel = p_stack_level; - for (int i = 1; i < on->arguments.size(); i++) { - int ret = _parse_expression(codegen, on->arguments[i], slevel); - if (ret < 0) { - return ret; - } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { - slevel++; - codegen.alloc_stack(slevel); - } - arguments.push_back(ret); + Vector<int> arguments; + int slevel = p_stack_level; + for (int i = 0; i < call->arguments.size(); i++) { + int ret = _parse_expression(codegen, call->arguments[i], slevel); + if (ret < 0) { + return ret; } - //push call bytecode - codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_SELF_BASE); // basic type constructor - - codegen.opcodes.push_back(codegen.get_name_map_pos(in->name)); //instance - codegen.opcodes.push_back(arguments.size()); //argument count - codegen.alloc_call(arguments.size()); - for (int i = 0; i < arguments.size(); i++) { - codegen.opcodes.push_back(arguments[i]); //arguments + if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); } - } break; - case GDScriptParser::OperatorNode::OP_CALL: { - if (on->arguments[0]->type == GDScriptParser::Node::TYPE_TYPE) { - //construct a basic type - ERR_FAIL_COND_V(on->arguments.size() < 1, -1); - - const GDScriptParser::TypeNode *tn = (const GDScriptParser::TypeNode *)on->arguments[0]; - int vtype = tn->vtype; - - Vector<int> arguments; - int slevel = p_stack_level; - for (int i = 1; i < on->arguments.size(); i++) { - int ret = _parse_expression(codegen, on->arguments[i], slevel); - if (ret < 0) { - return ret; - } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { - slevel++; - codegen.alloc_stack(slevel); - } - arguments.push_back(ret); - } + arguments.push_back(ret); + } - //push call bytecode - codegen.opcodes.push_back(GDScriptFunction::OPCODE_CONSTRUCT); // basic type constructor - codegen.opcodes.push_back(vtype); //instance - codegen.opcodes.push_back(arguments.size()); //argument count - codegen.alloc_call(arguments.size()); - for (int i = 0; i < arguments.size(); i++) { - codegen.opcodes.push_back(arguments[i]); //arguments - } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptParser::get_builtin_function(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name)); + codegen.opcodes.push_back(arguments.size()); + codegen.alloc_call(arguments.size()); + for (int i = 0; i < arguments.size(); i++) { + codegen.opcodes.push_back(arguments[i]); + } + + } else { + //regular function - } else if (on->arguments[0]->type == GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION) { - //built in function + const GDScriptParser::ExpressionNode *callee = call->callee; - ERR_FAIL_COND_V(on->arguments.size() < 1, -1); + Vector<int> arguments; + int slevel = p_stack_level; - Vector<int> arguments; - int slevel = p_stack_level; - for (int i = 1; i < on->arguments.size(); i++) { - int ret = _parse_expression(codegen, on->arguments[i], slevel); + // TODO: Use callables when possible if needed. + int ret = -1; + int super_address = -1; + if (call->is_super) { + // Super call. + if (call->callee == nullptr) { + // Implicit super function call. + super_address = codegen.get_name_map_pos(codegen.function_node->identifier->name); + } else { + super_address = codegen.get_name_map_pos(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name); + } + } else { + if (callee->type == GDScriptParser::Node::IDENTIFIER) { + // Self function call. + if ((codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") { + ret = (GDScriptFunction::ADDR_TYPE_CLASS << GDScriptFunction::ADDR_BITS); + } else { + ret = (GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); + } + arguments.push_back(ret); + ret = codegen.get_name_map_pos(static_cast<GDScriptParser::IdentifierNode *>(call->callee)->name); + arguments.push_back(ret); + } else if (callee->type == GDScriptParser::Node::SUBSCRIPT) { + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(call->callee); + + if (subscript->is_attribute) { + ret = _parse_expression(codegen, subscript->base, slevel); if (ret < 0) { return ret; } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { slevel++; codegen.alloc_stack(slevel); } - arguments.push_back(ret); + arguments.push_back(codegen.get_name_map_pos(subscript->attribute->name)); + } else { + _set_error("Cannot call something that isn't a function.", call->callee); + return -1; } - - codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); - codegen.opcodes.push_back(static_cast<const GDScriptParser::BuiltInFunctionNode *>(on->arguments[0])->function); - codegen.opcodes.push_back(on->arguments.size() - 1); - codegen.alloc_call(on->arguments.size() - 1); - for (int i = 0; i < arguments.size(); i++) { - codegen.opcodes.push_back(arguments[i]); - } - } else { - //regular function - ERR_FAIL_COND_V(on->arguments.size() < 2, -1); - - const GDScriptParser::Node *instance = on->arguments[0]; + _set_error("Cannot call something that isn't a function.", call->callee); + return -1; + } + } - if (instance->type == GDScriptParser::Node::TYPE_SELF) { - //room for optimization - } + for (int i = 0; i < call->arguments.size(); i++) { + ret = _parse_expression(codegen, call->arguments[i], slevel); + if (ret < 0) { + return ret; + } + if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); + } + arguments.push_back(ret); + } - Vector<int> arguments; - int slevel = p_stack_level; + int opcode = GDScriptFunction::OPCODE_CALL_RETURN; + if (call->is_super) { + opcode = GDScriptFunction::OPCODE_CALL_SELF_BASE; + } else if (within_await) { + opcode = GDScriptFunction::OPCODE_CALL_ASYNC; + } else if (p_root) { + opcode = GDScriptFunction::OPCODE_CALL; + } - for (int i = 0; i < on->arguments.size(); i++) { - int ret; + codegen.opcodes.push_back(opcode); // perform operator + if (call->is_super) { + codegen.opcodes.push_back(super_address); + } + codegen.opcodes.push_back(call->arguments.size()); + codegen.alloc_call(call->arguments.size()); + for (int i = 0; i < arguments.size(); i++) { + codegen.opcodes.push_back(arguments[i]); + } + } + OPERATOR_RETURN; + } break; + case GDScriptParser::Node::GET_NODE: { + const GDScriptParser::GetNodeNode *get_node = static_cast<const GDScriptParser::GetNodeNode *>(p_expression); - if (i == 0 && on->arguments[i]->type == GDScriptParser::Node::TYPE_SELF && codegen.function_node && codegen.function_node->_static) { - //static call to self - ret = (GDScriptFunction::ADDR_TYPE_CLASS << GDScriptFunction::ADDR_BITS); - } else if (i == 1) { - if (on->arguments[i]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - _set_error("Attempt to call a non-identifier.", on); - return -1; - } - GDScriptParser::IdentifierNode *id = static_cast<GDScriptParser::IdentifierNode *>(on->arguments[i]); - ret = codegen.get_name_map_pos(id->name); + String node_name; + if (get_node->string != nullptr) { + node_name += String(get_node->string->value); + } else { + for (int i = 0; i < get_node->chain.size(); i++) { + if (i > 0) { + node_name += "/"; + } + node_name += get_node->chain[i]->name; + } + } - } else { - ret = _parse_expression(codegen, on->arguments[i], slevel); - if (ret < 0) { - return ret; - } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { - slevel++; - codegen.alloc_stack(slevel); - } - } - arguments.push_back(ret); - } + int arg_address = codegen.get_constant_pos(NodePath(node_name)); - codegen.opcodes.push_back(p_root ? GDScriptFunction::OPCODE_CALL : GDScriptFunction::OPCODE_CALL_RETURN); // perform operator - codegen.opcodes.push_back(on->arguments.size() - 2); - codegen.alloc_call(on->arguments.size() - 2); - for (int i = 0; i < arguments.size(); i++) { - codegen.opcodes.push_back(arguments[i]); - } - } - } break; - case GDScriptParser::OperatorNode::OP_YIELD: { - ERR_FAIL_COND_V(on->arguments.size() && on->arguments.size() != 2, -1); + codegen.opcodes.push_back(p_root ? GDScriptFunction::OPCODE_CALL : GDScriptFunction::OPCODE_CALL_RETURN); + codegen.opcodes.push_back(1); // number of arguments. + codegen.alloc_call(1); + codegen.opcodes.push_back(GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); // self. + codegen.opcodes.push_back(codegen.get_name_map_pos("get_node")); // function. + codegen.opcodes.push_back(arg_address); // argument (NodePath). + OPERATOR_RETURN; + } break; + case GDScriptParser::Node::PRELOAD: { + const GDScriptParser::PreloadNode *preload = static_cast<const GDScriptParser::PreloadNode *>(p_expression); - Vector<int> arguments; - int slevel = p_stack_level; - for (int i = 0; i < on->arguments.size(); i++) { - int ret = _parse_expression(codegen, on->arguments[i], slevel); - if (ret < 0) { - return ret; - } - if ((ret >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { - slevel++; - codegen.alloc_stack(slevel); - } - arguments.push_back(ret); - } + // Add resource as constant. + return codegen.get_constant_pos(preload->resource); + } break; + case GDScriptParser::Node::AWAIT: { + const GDScriptParser::AwaitNode *await = static_cast<const GDScriptParser::AwaitNode *>(p_expression); - //push call bytecode - codegen.opcodes.push_back(arguments.size() == 0 ? GDScriptFunction::OPCODE_YIELD : GDScriptFunction::OPCODE_YIELD_SIGNAL); // basic type constructor - for (int i = 0; i < arguments.size(); i++) { - codegen.opcodes.push_back(arguments[i]); //arguments - } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_YIELD_RESUME); - //next will be where to place the result :) + int slevel = p_stack_level; + within_await = true; + int argument = _parse_expression(codegen, await->to_await, slevel); + within_await = false; + if (argument < 0) { + return argument; + } + if ((argument >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); + } + //push call bytecode + codegen.opcodes.push_back(GDScriptFunction::OPCODE_AWAIT); + codegen.opcodes.push_back(argument); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_AWAIT_RESUME); + //next will be where to place the result :) - } break; + OPERATOR_RETURN; + } break; - //indexing operator - case GDScriptParser::OperatorNode::OP_INDEX: - case GDScriptParser::OperatorNode::OP_INDEX_NAMED: { - ERR_FAIL_COND_V(on->arguments.size() != 2, -1); + //indexing operator + case GDScriptParser::Node::SUBSCRIPT: { + int slevel = p_stack_level; - int slevel = p_stack_level; - bool named = (on->op == GDScriptParser::OperatorNode::OP_INDEX_NAMED); + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(p_expression); - int from = _parse_expression(codegen, on->arguments[0], slevel); - if (from < 0) { - return from; - } + int from = _parse_expression(codegen, subscript->base, slevel); + if (from < 0) { + return from; + } - int index; - if (p_index_addr != 0) { - index = p_index_addr; - } else if (named) { - if (on->arguments[0]->type == GDScriptParser::Node::TYPE_SELF && codegen.script && codegen.function_node && !codegen.function_node->_static) { - GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(on->arguments[1]); - const Map<StringName, GDScript::MemberInfo>::Element *MI = codegen.script->member_indices.find(identifier->name); + bool named = subscript->is_attribute; + int index; + if (p_index_addr != 0) { + index = p_index_addr; + } else if (subscript->is_attribute) { + if (subscript->base->type == GDScriptParser::Node::SELF && codegen.script) { + GDScriptParser::IdentifierNode *identifier = subscript->attribute; + const Map<StringName, GDScript::MemberInfo>::Element *MI = codegen.script->member_indices.find(identifier->name); #ifdef DEBUG_ENABLED - if (MI && MI->get().getter == codegen.function_node->name) { - String n = static_cast<GDScriptParser::IdentifierNode *>(on->arguments[1])->name; - _set_error("Must use '" + n + "' instead of 'self." + n + "' in getter.", on); - return -1; - } + if (MI && MI->get().getter == codegen.function_name) { + String n = identifier->name; + _set_error("Must use '" + n + "' instead of 'self." + n + "' in getter.", identifier); + return -1; + } #endif - if (MI && MI->get().getter == "") { - // Faster than indexing self (as if no self. had been used) - return (MI->get().index) | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS); - } - } + if (MI && MI->get().getter == "") { + // Faster than indexing self (as if no self. had been used) + return (MI->get().index) | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS); + } + } - index = codegen.get_name_map_pos(static_cast<GDScriptParser::IdentifierNode *>(on->arguments[1])->name); + index = codegen.get_name_map_pos(subscript->attribute->name); - } else { - if (on->arguments[1]->type == GDScriptParser::Node::TYPE_CONSTANT && static_cast<const GDScriptParser::ConstantNode *>(on->arguments[1])->value.get_type() == Variant::STRING) { - //also, somehow, named (speed up anyway) - StringName name = static_cast<const GDScriptParser::ConstantNode *>(on->arguments[1])->value; - index = codegen.get_name_map_pos(name); - named = true; - - } else { - //regular indexing - if (from & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; - codegen.alloc_stack(slevel); - } + } else { + if (subscript->index->type == GDScriptParser::Node::LITERAL && static_cast<const GDScriptParser::LiteralNode *>(subscript->index)->value.get_type() == Variant::STRING) { + //also, somehow, named (speed up anyway) + StringName name = static_cast<const GDScriptParser::LiteralNode *>(subscript->index)->value; + index = codegen.get_name_map_pos(name); + named = true; - index = _parse_expression(codegen, on->arguments[1], slevel); - if (index < 0) { - return index; - } - } + } else { + //regular indexing + if (from & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { + slevel++; + codegen.alloc_stack(slevel); } - codegen.opcodes.push_back(named ? GDScriptFunction::OPCODE_GET_NAMED : GDScriptFunction::OPCODE_GET); // perform operator - codegen.opcodes.push_back(from); // argument 1 - codegen.opcodes.push_back(index); // argument 2 (unary only takes one parameter) + index = _parse_expression(codegen, subscript->index, slevel); + if (index < 0) { + return index; + } + } + } + codegen.opcodes.push_back(named ? GDScriptFunction::OPCODE_GET_NAMED : GDScriptFunction::OPCODE_GET); // perform operator + codegen.opcodes.push_back(from); // argument 1 + codegen.opcodes.push_back(index); // argument 2 (unary only takes one parameter) + OPERATOR_RETURN; + } break; + case GDScriptParser::Node::UNARY_OPERATOR: { + //unary operators + const GDScriptParser::UnaryOpNode *unary = static_cast<const GDScriptParser::UnaryOpNode *>(p_expression); + switch (unary->operation) { + case GDScriptParser::UnaryOpNode::OP_NEGATIVE: { + if (!_create_unary_operator(codegen, unary, Variant::OP_NEGATE, p_stack_level)) { + return -1; + } + } break; + case GDScriptParser::UnaryOpNode::OP_POSITIVE: { + if (!_create_unary_operator(codegen, unary, Variant::OP_POSITIVE, p_stack_level)) { + return -1; + } + } break; + case GDScriptParser::UnaryOpNode::OP_LOGIC_NOT: { + if (!_create_unary_operator(codegen, unary, Variant::OP_NOT, p_stack_level)) { + return -1; + } } break; - case GDScriptParser::OperatorNode::OP_AND: { + case GDScriptParser::UnaryOpNode::OP_COMPLEMENT: { + if (!_create_unary_operator(codegen, unary, Variant::OP_BIT_NEGATE, p_stack_level)) { + return -1; + } + } break; + } + OPERATOR_RETURN; + } + case GDScriptParser::Node::BINARY_OPERATOR: { + //binary operators (in precedence order) + const GDScriptParser::BinaryOpNode *binary = static_cast<const GDScriptParser::BinaryOpNode *>(p_expression); + + switch (binary->operation) { + case GDScriptParser::BinaryOpNode::OP_LOGIC_AND: { // AND operator with early out on failure - int res = _parse_expression(codegen, on->arguments[0], p_stack_level); + int res = _parse_expression(codegen, binary->left_operand, p_stack_level); if (res < 0) { return res; } @@ -769,7 +961,7 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: int jump_fail_pos = codegen.opcodes.size(); codegen.opcodes.push_back(0); - res = _parse_expression(codegen, on->arguments[1], p_stack_level); + res = _parse_expression(codegen, binary->right_operand, p_stack_level); if (res < 0) { return res; } @@ -791,10 +983,10 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; } break; - case GDScriptParser::OperatorNode::OP_OR: { + case GDScriptParser::BinaryOpNode::OP_LOGIC_OR: { // OR operator with early out on success - int res = _parse_expression(codegen, on->arguments[0], p_stack_level); + int res = _parse_expression(codegen, binary->left_operand, p_stack_level); if (res < 0) { return res; } @@ -803,7 +995,7 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: int jump_success_pos = codegen.opcodes.size(); codegen.opcodes.push_back(0); - res = _parse_expression(codegen, on->arguments[1], p_stack_level); + res = _parse_expression(codegen, binary->right_operand, p_stack_level); if (res < 0) { return res; } @@ -825,736 +1017,1020 @@ int GDScriptCompiler::_parse_expression(CodeGen &codegen, const GDScriptParser:: return p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; } break; - // ternary operators - case GDScriptParser::OperatorNode::OP_TERNARY_IF: { - // x IF a ELSE y operator with early out on failure + case GDScriptParser::BinaryOpNode::OP_TYPE_TEST: { + int slevel = p_stack_level; - int res = _parse_expression(codegen, on->arguments[0], p_stack_level); - if (res < 0) { - return res; + int src_address_a = _parse_expression(codegen, binary->left_operand, slevel); + if (src_address_a < 0) { + return -1; } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); - codegen.opcodes.push_back(res); - int jump_fail_pos = codegen.opcodes.size(); - codegen.opcodes.push_back(0); - res = _parse_expression(codegen, on->arguments[1], p_stack_level); - if (res < 0) { - return res; + if (src_address_a & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { + slevel++; //uses stack for return, increase stack } - codegen.alloc_stack(p_stack_level); //it will be used.. - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); - codegen.opcodes.push_back(p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - codegen.opcodes.push_back(res); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - int jump_past_pos = codegen.opcodes.size(); - codegen.opcodes.push_back(0); - - codegen.opcodes.write[jump_fail_pos] = codegen.opcodes.size(); - res = _parse_expression(codegen, on->arguments[2], p_stack_level); - if (res < 0) { - return res; + int src_address_b = -1; + bool builtin = false; + if (binary->right_operand->type == GDScriptParser::Node::IDENTIFIER && GDScriptParser::get_builtin_type(static_cast<const GDScriptParser::IdentifierNode *>(binary->right_operand)->name) != Variant::VARIANT_MAX) { + // `is` with builtin type + builtin = true; + src_address_b = (int)GDScriptParser::get_builtin_type(static_cast<const GDScriptParser::IdentifierNode *>(binary->right_operand)->name); + } else { + src_address_b = _parse_expression(codegen, binary->right_operand, slevel); + if (src_address_b < 0) { + return -1; + } } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); - codegen.opcodes.push_back(p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - codegen.opcodes.push_back(res); - - codegen.opcodes.write[jump_past_pos] = codegen.opcodes.size(); - - return p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.opcodes.push_back(builtin ? GDScriptFunction::OPCODE_IS_BUILTIN : GDScriptFunction::OPCODE_EXTENDS_TEST); // perform operator + codegen.opcodes.push_back(src_address_a); // argument 1 + codegen.opcodes.push_back(src_address_b); // argument 2 (unary only takes one parameter) } break; - //unary operators - case GDScriptParser::OperatorNode::OP_NEG: { - if (!_create_unary_operator(codegen, on, Variant::OP_NEGATE, p_stack_level)) { - return -1; - } - } break; - case GDScriptParser::OperatorNode::OP_POS: { - if (!_create_unary_operator(codegen, on, Variant::OP_POSITIVE, p_stack_level)) { - return -1; - } - } break; - case GDScriptParser::OperatorNode::OP_NOT: { - if (!_create_unary_operator(codegen, on, Variant::OP_NOT, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_CONTENT_TEST: { + if (!_create_binary_operator(codegen, binary, Variant::OP_IN, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_BIT_INVERT: { - if (!_create_unary_operator(codegen, on, Variant::OP_BIT_NEGATE, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_EQUAL: { + if (!_create_binary_operator(codegen, binary, Variant::OP_EQUAL, p_stack_level)) { return -1; } } break; - //binary operators (in precedence order) - case GDScriptParser::OperatorNode::OP_IN: { - if (!_create_binary_operator(codegen, on, Variant::OP_IN, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_NOT_EQUAL: { + if (!_create_binary_operator(codegen, binary, Variant::OP_NOT_EQUAL, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_EQUAL: { - if (!_create_binary_operator(codegen, on, Variant::OP_EQUAL, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_LESS: { + if (!_create_binary_operator(codegen, binary, Variant::OP_LESS, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_NOT_EQUAL: { - if (!_create_binary_operator(codegen, on, Variant::OP_NOT_EQUAL, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_LESS_EQUAL: { + if (!_create_binary_operator(codegen, binary, Variant::OP_LESS_EQUAL, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_LESS: { - if (!_create_binary_operator(codegen, on, Variant::OP_LESS, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_GREATER: { + if (!_create_binary_operator(codegen, binary, Variant::OP_GREATER, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_LESS_EQUAL: { - if (!_create_binary_operator(codegen, on, Variant::OP_LESS_EQUAL, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_COMP_GREATER_EQUAL: { + if (!_create_binary_operator(codegen, binary, Variant::OP_GREATER_EQUAL, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_GREATER: { - if (!_create_binary_operator(codegen, on, Variant::OP_GREATER, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_ADDITION: { + if (!_create_binary_operator(codegen, binary, Variant::OP_ADD, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_GREATER_EQUAL: { - if (!_create_binary_operator(codegen, on, Variant::OP_GREATER_EQUAL, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_SUBTRACTION: { + if (!_create_binary_operator(codegen, binary, Variant::OP_SUBTRACT, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_ADD: { - if (!_create_binary_operator(codegen, on, Variant::OP_ADD, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_MULTIPLICATION: { + if (!_create_binary_operator(codegen, binary, Variant::OP_MULTIPLY, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_SUB: { - if (!_create_binary_operator(codegen, on, Variant::OP_SUBTRACT, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_DIVISION: { + if (!_create_binary_operator(codegen, binary, Variant::OP_DIVIDE, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_MUL: { - if (!_create_binary_operator(codegen, on, Variant::OP_MULTIPLY, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_MODULO: { + if (!_create_binary_operator(codegen, binary, Variant::OP_MODULE, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_DIV: { - if (!_create_binary_operator(codegen, on, Variant::OP_DIVIDE, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_BIT_AND: { + if (!_create_binary_operator(codegen, binary, Variant::OP_BIT_AND, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_MOD: { - if (!_create_binary_operator(codegen, on, Variant::OP_MODULE, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_BIT_OR: { + if (!_create_binary_operator(codegen, binary, Variant::OP_BIT_OR, p_stack_level)) { return -1; } } break; - //case GDScriptParser::OperatorNode::OP_SHIFT_LEFT: { if (!_create_binary_operator(codegen,on,Variant::OP_SHIFT_LEFT,p_stack_level)) return -1;} break; - //case GDScriptParser::OperatorNode::OP_SHIFT_RIGHT: { if (!_create_binary_operator(codegen,on,Variant::OP_SHIFT_RIGHT,p_stack_level)) return -1;} break; - case GDScriptParser::OperatorNode::OP_BIT_AND: { - if (!_create_binary_operator(codegen, on, Variant::OP_BIT_AND, p_stack_level)) { - return -1; - } - } break; - case GDScriptParser::OperatorNode::OP_BIT_OR: { - if (!_create_binary_operator(codegen, on, Variant::OP_BIT_OR, p_stack_level)) { - return -1; - } - } break; - case GDScriptParser::OperatorNode::OP_BIT_XOR: { - if (!_create_binary_operator(codegen, on, Variant::OP_BIT_XOR, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_BIT_XOR: { + if (!_create_binary_operator(codegen, binary, Variant::OP_BIT_XOR, p_stack_level)) { return -1; } } break; //shift - case GDScriptParser::OperatorNode::OP_SHIFT_LEFT: { - if (!_create_binary_operator(codegen, on, Variant::OP_SHIFT_LEFT, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_BIT_LEFT_SHIFT: { + if (!_create_binary_operator(codegen, binary, Variant::OP_SHIFT_LEFT, p_stack_level)) { return -1; } } break; - case GDScriptParser::OperatorNode::OP_SHIFT_RIGHT: { - if (!_create_binary_operator(codegen, on, Variant::OP_SHIFT_RIGHT, p_stack_level)) { + case GDScriptParser::BinaryOpNode::OP_BIT_RIGHT_SHIFT: { + if (!_create_binary_operator(codegen, binary, Variant::OP_SHIFT_RIGHT, p_stack_level)) { return -1; } } break; - //assignment operators - case GDScriptParser::OperatorNode::OP_ASSIGN_ADD: - case GDScriptParser::OperatorNode::OP_ASSIGN_SUB: - case GDScriptParser::OperatorNode::OP_ASSIGN_MUL: - case GDScriptParser::OperatorNode::OP_ASSIGN_DIV: - case GDScriptParser::OperatorNode::OP_ASSIGN_MOD: - case GDScriptParser::OperatorNode::OP_ASSIGN_SHIFT_LEFT: - case GDScriptParser::OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_AND: - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_OR: - case GDScriptParser::OperatorNode::OP_ASSIGN_BIT_XOR: - case GDScriptParser::OperatorNode::OP_INIT_ASSIGN: - case GDScriptParser::OperatorNode::OP_ASSIGN: { - ERR_FAIL_COND_V(on->arguments.size() != 2, -1); - - if (on->arguments[0]->type == GDScriptParser::Node::TYPE_OPERATOR && (static_cast<GDScriptParser::OperatorNode *>(on->arguments[0])->op == GDScriptParser::OperatorNode::OP_INDEX || static_cast<GDScriptParser::OperatorNode *>(on->arguments[0])->op == GDScriptParser::OperatorNode::OP_INDEX_NAMED)) { - // SET (chained) MODE! -#ifdef DEBUG_ENABLED - if (static_cast<GDScriptParser::OperatorNode *>(on->arguments[0])->op == GDScriptParser::OperatorNode::OP_INDEX_NAMED) { - const GDScriptParser::OperatorNode *inon = static_cast<GDScriptParser::OperatorNode *>(on->arguments[0]); - - if (inon->arguments[0]->type == GDScriptParser::Node::TYPE_SELF && codegen.script && codegen.function_node && !codegen.function_node->_static) { - const Map<StringName, GDScript::MemberInfo>::Element *MI = codegen.script->member_indices.find(static_cast<GDScriptParser::IdentifierNode *>(inon->arguments[1])->name); - if (MI && MI->get().setter == codegen.function_node->name) { - String n = static_cast<GDScriptParser::IdentifierNode *>(inon->arguments[1])->name; - _set_error("Must use '" + n + "' instead of 'self." + n + "' in setter.", inon); - return -1; - } - } - } -#endif + } + OPERATOR_RETURN; + } break; + // ternary operators + case GDScriptParser::Node::TERNARY_OPERATOR: { + // x IF a ELSE y operator with early out on failure + + const GDScriptParser::TernaryOpNode *ternary = static_cast<const GDScriptParser::TernaryOpNode *>(p_expression); + int res = _parse_expression(codegen, ternary->condition, p_stack_level); + if (res < 0) { + return res; + } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(res); + int jump_fail_pos = codegen.opcodes.size(); + codegen.opcodes.push_back(0); - int slevel = p_stack_level; + res = _parse_expression(codegen, ternary->true_expr, p_stack_level); + if (res < 0) { + return res; + } - GDScriptParser::OperatorNode *op = static_cast<GDScriptParser::OperatorNode *>(on->arguments[0]); + codegen.alloc_stack(p_stack_level); //it will be used.. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); + codegen.opcodes.push_back(p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + codegen.opcodes.push_back(res); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + int jump_past_pos = codegen.opcodes.size(); + codegen.opcodes.push_back(0); - /* Find chain of sets */ + codegen.opcodes.write[jump_fail_pos] = codegen.opcodes.size(); + res = _parse_expression(codegen, ternary->false_expr, p_stack_level); + if (res < 0) { + return res; + } - StringName assign_property; + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); + codegen.opcodes.push_back(p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + codegen.opcodes.push_back(res); - List<GDScriptParser::OperatorNode *> chain; + codegen.opcodes.write[jump_past_pos] = codegen.opcodes.size(); - { - //create get/set chain - GDScriptParser::OperatorNode *n = op; - while (true) { - chain.push_back(n); - if (n->arguments[0]->type != GDScriptParser::Node::TYPE_OPERATOR) { - //check for a built-in property - if (n->arguments[0]->type == GDScriptParser::Node::TYPE_IDENTIFIER) { - GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(n->arguments[0]); - if (_is_class_member_property(codegen, identifier->name)) { - assign_property = identifier->name; - } - } - break; - } - n = static_cast<GDScriptParser::OperatorNode *>(n->arguments[0]); - if (n->op != GDScriptParser::OperatorNode::OP_INDEX && n->op != GDScriptParser::OperatorNode::OP_INDEX_NAMED) { - break; - } - } - } + return p_stack_level | GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; - /* Chain of gets */ + } break; + //assignment operators + case GDScriptParser::Node::ASSIGNMENT: { + const GDScriptParser::AssignmentNode *assignment = static_cast<const GDScriptParser::AssignmentNode *>(p_expression); - //get at (potential) root stack pos, so it can be returned - int prev_pos = _parse_expression(codegen, chain.back()->get()->arguments[0], slevel); - if (prev_pos < 0) { - return prev_pos; + if (assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) { + // SET (chained) MODE! + const GDScriptParser::SubscriptNode *subscript = static_cast<GDScriptParser::SubscriptNode *>(assignment->assignee); +#ifdef DEBUG_ENABLED + if (subscript->is_attribute) { + if (subscript->base->type == GDScriptParser::Node::SELF && codegen.script) { + const Map<StringName, GDScript::MemberInfo>::Element *MI = codegen.script->member_indices.find(subscript->attribute->name); + if (MI && MI->get().setter == codegen.function_name) { + String n = subscript->attribute->name; + _set_error("Must use '" + n + "' instead of 'self." + n + "' in setter.", subscript); + return -1; } - int retval = prev_pos; + } + } +#endif - if (retval & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; - codegen.alloc_stack(slevel); - } + int slevel = p_stack_level; - Vector<int> setchain; + /* Find chain of sets */ - if (assign_property != StringName()) { - // recover and assign at the end, this allows stuff like - // position.x+=2.0 - // in Node2D - setchain.push_back(prev_pos); - setchain.push_back(codegen.get_name_map_pos(assign_property)); - setchain.push_back(GDScriptFunction::OPCODE_SET_MEMBER); - } + StringName assign_property; + + List<const GDScriptParser::SubscriptNode *> chain; + + { + //create get/set chain + const GDScriptParser::SubscriptNode *n = subscript; + while (true) { + chain.push_back(n); - for (List<GDScriptParser::OperatorNode *>::Element *E = chain.back(); E; E = E->prev()) { - if (E == chain.front()) { //ignore first - break; + if (n->base->type != GDScriptParser::Node::SUBSCRIPT) { + //check for a built-in property + if (n->base->type == GDScriptParser::Node::IDENTIFIER) { + GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode *>(n->base); + if (_is_class_member_property(codegen, identifier->name)) { + assign_property = identifier->name; + } } + break; + } + n = static_cast<const GDScriptParser::SubscriptNode *>(n->base); + } + } - bool named = E->get()->op == GDScriptParser::OperatorNode::OP_INDEX_NAMED; - int key_idx; + /* Chain of gets */ - if (named) { - key_idx = codegen.get_name_map_pos(static_cast<const GDScriptParser::IdentifierNode *>(E->get()->arguments[1])->name); - //printf("named key %x\n",key_idx); + //get at (potential) root stack pos, so it can be returned + int prev_pos = _parse_expression(codegen, chain.back()->get()->base, slevel); + if (prev_pos < 0) { + return prev_pos; + } + int retval = prev_pos; - } else { - if (prev_pos & (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS)) { - slevel++; - codegen.alloc_stack(slevel); - } + if (retval & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { + slevel++; + codegen.alloc_stack(slevel); + } - GDScriptParser::Node *key = E->get()->arguments[1]; - key_idx = _parse_expression(codegen, key, slevel); - //printf("expr key %x\n",key_idx); + Vector<int> setchain; - //stack was raised here if retval was stack but.. - } + if (assign_property != StringName()) { + // recover and assign at the end, this allows stuff like + // position.x+=2.0 + // in Node2D + setchain.push_back(prev_pos); + setchain.push_back(codegen.get_name_map_pos(assign_property)); + setchain.push_back(GDScriptFunction::OPCODE_SET_MEMBER); + } - if (key_idx < 0) { //error - return key_idx; - } + for (List<const GDScriptParser::SubscriptNode *>::Element *E = chain.back(); E; E = E->prev()) { + if (E == chain.front()) { //ignore first + break; + } + + const GDScriptParser::SubscriptNode *subscript_elem = E->get(); + int key_idx; - codegen.opcodes.push_back(named ? GDScriptFunction::OPCODE_GET_NAMED : GDScriptFunction::OPCODE_GET); - codegen.opcodes.push_back(prev_pos); - codegen.opcodes.push_back(key_idx); + if (subscript_elem->is_attribute) { + key_idx = codegen.get_name_map_pos(subscript_elem->attribute->name); + //printf("named key %x\n",key_idx); + + } else { + if (prev_pos & (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS)) { slevel++; codegen.alloc_stack(slevel); - int dst_pos = (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) | slevel; - - codegen.opcodes.push_back(dst_pos); + } - //add in reverse order, since it will be reverted + GDScriptParser::ExpressionNode *key = subscript_elem->index; + key_idx = _parse_expression(codegen, key, slevel); + //printf("expr key %x\n",key_idx); - setchain.push_back(dst_pos); - setchain.push_back(key_idx); - setchain.push_back(prev_pos); - setchain.push_back(named ? GDScriptFunction::OPCODE_SET_NAMED : GDScriptFunction::OPCODE_SET); + //stack was raised here if retval was stack but.. + } - prev_pos = dst_pos; - } + if (key_idx < 0) { //error + return key_idx; + } - setchain.invert(); + codegen.opcodes.push_back(subscript_elem->is_attribute ? GDScriptFunction::OPCODE_GET_NAMED : GDScriptFunction::OPCODE_GET); + codegen.opcodes.push_back(prev_pos); + codegen.opcodes.push_back(key_idx); + slevel++; + codegen.alloc_stack(slevel); + int dst_pos = (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) | slevel; - int set_index; - bool named = false; + codegen.opcodes.push_back(dst_pos); - if (op->op == GDScriptParser::OperatorNode::OP_INDEX_NAMED) { - set_index = codegen.get_name_map_pos(static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[1])->name); - named = true; - } else { - set_index = _parse_expression(codegen, op->arguments[1], slevel + 1); - named = false; - } + //add in reverse order, since it will be reverted - if (set_index < 0) { //error - return set_index; - } + setchain.push_back(dst_pos); + setchain.push_back(key_idx); + setchain.push_back(prev_pos); + setchain.push_back(subscript_elem->is_attribute ? GDScriptFunction::OPCODE_SET_NAMED : GDScriptFunction::OPCODE_SET); - if (set_index & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; - codegen.alloc_stack(slevel); - } + prev_pos = dst_pos; + } - int set_value = _parse_assign_right_expression(codegen, on, slevel + 1, named ? 0 : set_index); - if (set_value < 0) { //error - return set_value; - } + setchain.invert(); - codegen.opcodes.push_back(named ? GDScriptFunction::OPCODE_SET_NAMED : GDScriptFunction::OPCODE_SET); - codegen.opcodes.push_back(prev_pos); - codegen.opcodes.push_back(set_index); - codegen.opcodes.push_back(set_value); + int set_index; - for (int i = 0; i < setchain.size(); i++) { - codegen.opcodes.push_back(setchain[i]); - } + if (subscript->is_attribute) { + set_index = codegen.get_name_map_pos(subscript->attribute->name); + } else { + set_index = _parse_expression(codegen, subscript->index, slevel + 1); + } - return retval; + if (set_index < 0) { //error + return set_index; + } - } else if (on->arguments[0]->type == GDScriptParser::Node::TYPE_IDENTIFIER && _is_class_member_property(codegen, static_cast<GDScriptParser::IdentifierNode *>(on->arguments[0])->name)) { - //assignment to member property + if (set_index & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { + slevel++; + codegen.alloc_stack(slevel); + } - int slevel = p_stack_level; + int set_value = _parse_assign_right_expression(codegen, assignment, slevel + 1, subscript->is_attribute ? 0 : set_index); + if (set_value < 0) { //error + return set_value; + } - int src_address = _parse_assign_right_expression(codegen, on, slevel); - if (src_address < 0) { - return -1; - } + codegen.opcodes.push_back(subscript->is_attribute ? GDScriptFunction::OPCODE_SET_NAMED : GDScriptFunction::OPCODE_SET); + codegen.opcodes.push_back(prev_pos); + codegen.opcodes.push_back(set_index); + codegen.opcodes.push_back(set_value); - StringName name = static_cast<GDScriptParser::IdentifierNode *>(on->arguments[0])->name; + for (int i = 0; i < setchain.size(); i++) { + codegen.opcodes.push_back(setchain[i]); + } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_SET_MEMBER); - codegen.opcodes.push_back(codegen.get_name_map_pos(name)); - codegen.opcodes.push_back(src_address); + return retval; - return GDScriptFunction::ADDR_TYPE_NIL << GDScriptFunction::ADDR_BITS; - } else { - //REGULAR ASSIGNMENT MODE!! + } else if (assignment->assignee->type == GDScriptParser::Node::IDENTIFIER && _is_class_member_property(codegen, static_cast<GDScriptParser::IdentifierNode *>(assignment->assignee)->name)) { + //assignment to member property - int slevel = p_stack_level; + int slevel = p_stack_level; - int dst_address_a = _parse_expression(codegen, on->arguments[0], slevel, false, on->op == GDScriptParser::OperatorNode::OP_INIT_ASSIGN); - if (dst_address_a < 0) { - return -1; - } + int src_address = _parse_assign_right_expression(codegen, assignment, slevel); + if (src_address < 0) { + return -1; + } - if (dst_address_a & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; - codegen.alloc_stack(slevel); - } + StringName name = static_cast<GDScriptParser::IdentifierNode *>(assignment->assignee)->name; - int src_address_b = _parse_assign_right_expression(codegen, on, slevel); - if (src_address_b < 0) { - return -1; - } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_SET_MEMBER); + codegen.opcodes.push_back(codegen.get_name_map_pos(name)); + codegen.opcodes.push_back(src_address); - GDScriptDataType assign_type = _gdtype_from_datatype(on->arguments[0]->get_datatype()); - - if (assign_type.has_type && !on->datatype.has_type) { - // Typed assignment - switch (assign_type.kind) { - case GDScriptDataType::BUILTIN: { - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_BUILTIN); // perform operator - codegen.opcodes.push_back(assign_type.builtin_type); // variable type - codegen.opcodes.push_back(dst_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 - } break; - case GDScriptDataType::NATIVE: { - int class_idx; - if (GDScriptLanguage::get_singleton()->get_global_map().has(assign_type.native_type)) { - class_idx = GDScriptLanguage::get_singleton()->get_global_map()[assign_type.native_type]; - class_idx |= (GDScriptFunction::ADDR_TYPE_GLOBAL << GDScriptFunction::ADDR_BITS); //argument (stack root) - } else { - _set_error("Invalid native class type '" + String(assign_type.native_type) + "'.", on->arguments[0]); - return -1; - } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_NATIVE); // perform operator - codegen.opcodes.push_back(class_idx); // variable type - codegen.opcodes.push_back(dst_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 - } break; - case GDScriptDataType::SCRIPT: - case GDScriptDataType::GDSCRIPT: { - Variant script = assign_type.script_type; - int idx = codegen.get_constant_pos(script); - idx |= GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS; //make it a local constant (faster access) - - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN_TYPED_SCRIPT); // perform operator - codegen.opcodes.push_back(idx); // variable type - codegen.opcodes.push_back(dst_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 - } break; - default: { - ERR_PRINT("Compiler bug: unresolved assign."); - - // Shouldn't get here, but fail-safe to a regular assignment - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); // perform operator - codegen.opcodes.push_back(dst_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 (unary only takes one parameter) - } - } - } else { - // Either untyped assignment or already type-checked by the parser - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); // perform operator - codegen.opcodes.push_back(dst_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 (unary only takes one parameter) + return GDScriptFunction::ADDR_TYPE_NIL << GDScriptFunction::ADDR_BITS; + } else { + //REGULAR ASSIGNMENT MODE!! + + int slevel = p_stack_level; + int dst_address_a = -1; + + bool has_setter = false; + bool is_in_setter = false; + StringName setter_function; + if (assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) { + StringName var_name = static_cast<const GDScriptParser::IdentifierNode *>(assignment->assignee)->name; + if (!codegen.stack_identifiers.has(var_name) && codegen.script->member_indices.has(var_name)) { + setter_function = codegen.script->member_indices[var_name].setter; + if (setter_function != StringName()) { + has_setter = true; + is_in_setter = setter_function == codegen.function_name; + dst_address_a = codegen.script->member_indices[var_name].index; } - return dst_address_a; //if anything, returns wathever was assigned or correct stack position } - } break; - case GDScriptParser::OperatorNode::OP_IS: { - ERR_FAIL_COND_V(on->arguments.size() != 2, false); - - int slevel = p_stack_level; + } - int src_address_a = _parse_expression(codegen, on->arguments[0], slevel); - if (src_address_a < 0) { + if (has_setter) { + if (is_in_setter) { + // Use direct member access. + dst_address_a |= GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS; + } else { + // Store stack slot for the temp value. + dst_address_a = slevel++; + codegen.alloc_stack(slevel); + dst_address_a |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + } + } else { + dst_address_a = _parse_expression(codegen, assignment->assignee, slevel); + if (dst_address_a < 0) { return -1; } - if (src_address_a & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; //uses stack for return, increase stack + if (dst_address_a & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { + slevel++; + codegen.alloc_stack(slevel); } + } - int src_address_b = _parse_expression(codegen, on->arguments[1], slevel); - if (src_address_b < 0) { - return -1; - } + int src_address_b = _parse_assign_right_expression(codegen, assignment, slevel); + if (src_address_b < 0) { + return -1; + } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_EXTENDS_TEST); // perform operator - codegen.opcodes.push_back(src_address_a); // argument 1 - codegen.opcodes.push_back(src_address_b); // argument 2 (unary only takes one parameter) + GDScriptDataType assign_type = _gdtype_from_datatype(assignment->assignee->get_datatype()); + + if (has_setter && !is_in_setter) { + // Call setter. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL); + codegen.opcodes.push_back(1); // Argument count. + codegen.opcodes.push_back(GDScriptFunction::ADDR_TYPE_SELF << GDScriptFunction::ADDR_BITS); // Base (self). + codegen.opcodes.push_back(codegen.get_name_map_pos(setter_function)); // Method name. + codegen.opcodes.push_back(dst_address_a); // Argument. + codegen.opcodes.push_back(dst_address_a); // Result address (won't be used here). + codegen.alloc_call(1); + } else if (!_generate_typed_assign(codegen, src_address_b, dst_address_a, assign_type, assignment->assigned_value->get_datatype())) { + return -1; + } - } break; - case GDScriptParser::OperatorNode::OP_IS_BUILTIN: { - ERR_FAIL_COND_V(on->arguments.size() != 2, false); - ERR_FAIL_COND_V(on->arguments[1]->type != GDScriptParser::Node::TYPE_TYPE, false); + return dst_address_a; //if anything, returns wathever was assigned or correct stack position + } + } break; +#undef OPERATOR_RETURN + //TYPE_TYPE, + default: { + ERR_FAIL_V_MSG(-1, "Bug in bytecode compiler, unexpected node in parse tree while parsing expression."); //unreachable code + } break; + } +} - int slevel = p_stack_level; +Error GDScriptCompiler::_parse_match_pattern(CodeGen &codegen, const GDScriptParser::PatternNode *p_pattern, int p_stack_level, int p_value_addr, int p_type_addr, int &r_bound_variables, Vector<int> &r_patch_addresses, Vector<int> &r_block_patch_address) { + // TODO: Many "repeated" code here that could be abstracted. This compiler is going away when new VM arrives though, so... + switch (p_pattern->pattern_type) { + case GDScriptParser::PatternNode::PT_LITERAL: { + // Get literal type into constant map. + int literal_type_addr = -1; + if (!codegen.constant_map.has((int)p_pattern->literal->value.get_type())) { + literal_type_addr = codegen.constant_map.size(); + codegen.constant_map[(int)p_pattern->literal->value.get_type()] = literal_type_addr; - int src_address_a = _parse_expression(codegen, on->arguments[0], slevel); - if (src_address_a < 0) { - return -1; - } + } else { + literal_type_addr = codegen.constant_map[(int)p_pattern->literal->value.get_type()]; + } + literal_type_addr |= GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS; - if (src_address_a & GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) { - slevel++; //uses stack for return, increase stack - } + // Check type equality. + int equality_addr = p_stack_level++; + equality_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(p_stack_level); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_type_addr); + codegen.opcodes.push_back(literal_type_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if not the same type. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Get literal. + int literal_addr = _parse_expression(codegen, p_pattern->literal, p_stack_level); + + // Check value equality. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_value_addr); + codegen.opcodes.push_back(literal_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if doesn't match. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Jump to the actual block since it matches. This is needed to take multi-pattern into account. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + r_block_patch_address.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + } break; + case GDScriptParser::PatternNode::PT_EXPRESSION: { + // Evaluate expression. + int expr_addr = _parse_expression(codegen, p_pattern->expression, p_stack_level); + if ((expr_addr >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + p_stack_level++; + codegen.alloc_stack(p_stack_level); + } - const GDScriptParser::TypeNode *tn = static_cast<const GDScriptParser::TypeNode *>(on->arguments[1]); + // Evaluate expression type. + int expr_type_addr = p_stack_level++; + expr_type_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(p_stack_level); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::TYPE_OF); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(expr_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(expr_type_addr); // Address to result. + + // Check type equality. + int equality_addr = p_stack_level++; + equality_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(p_stack_level); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_type_addr); + codegen.opcodes.push_back(expr_type_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if not the same type. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Check value equality. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_value_addr); + codegen.opcodes.push_back(expr_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if doesn't match. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Jump to the actual block since it matches. This is needed to take multi-pattern into account. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + r_block_patch_address.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + } break; + case GDScriptParser::PatternNode::PT_BIND: { + // Create new stack variable. + int bind_addr = p_stack_level | (GDScriptFunction::ADDR_TYPE_STACK_VARIABLE << GDScriptFunction::ADDR_BITS); + codegen.add_stack_identifier(p_pattern->bind->name, p_stack_level++); + codegen.alloc_stack(p_stack_level); + r_bound_variables++; - codegen.opcodes.push_back(GDScriptFunction::OPCODE_IS_BUILTIN); // perform operator - codegen.opcodes.push_back(src_address_a); // argument 1 - codegen.opcodes.push_back((int)tn->vtype); // argument 2 (unary only takes one parameter) - } break; - default: { - ERR_FAIL_V_MSG(0, "Bug in bytecode compiler, unexpected operator #" + itos(on->op) + " in parse tree while parsing expression."); //unreachable code + // Assign value to bound variable. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); + codegen.opcodes.push_back(bind_addr); // Destination. + codegen.opcodes.push_back(p_value_addr); // Source. + // Not need to block jump because bind happens only once. + } break; + case GDScriptParser::PatternNode::PT_ARRAY: { + int slevel = p_stack_level; - } break; + // Get array type into constant map. + int array_type_addr = codegen.get_constant_pos(Variant::ARRAY); + + // Check type equality. + int equality_addr = slevel++; + equality_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(slevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_type_addr); + codegen.opcodes.push_back(array_type_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if not the same type. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Store pattern length in constant map. + int array_length_addr = codegen.get_constant_pos(p_pattern->rest_used ? p_pattern->array.size() - 1 : p_pattern->array.size()); + + // Get value length. + int value_length_addr = slevel++; + codegen.alloc_stack(slevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::LEN); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(p_value_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(value_length_addr); // Address to result. + + // Test length compatibility. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(p_pattern->rest_used ? Variant::OP_GREATER_EQUAL : Variant::OP_EQUAL); + codegen.opcodes.push_back(value_length_addr); + codegen.opcodes.push_back(array_length_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if length is not compatible. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Evaluate element by element. + for (int i = 0; i < p_pattern->array.size(); i++) { + if (p_pattern->array[i]->pattern_type == GDScriptParser::PatternNode::PT_REST) { + // Don't want to access an extra element of the user array. + break; + } + + int stlevel = p_stack_level; + Vector<int> element_block_patches; // I want to internal patterns try the next element instead of going to the block. + // Add index to constant map. + int index_addr = codegen.get_constant_pos(i); + + // Get the actual element from the user-sent array. + int element_addr = stlevel++; + codegen.alloc_stack(stlevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_GET); + codegen.opcodes.push_back(p_value_addr); // Source. + codegen.opcodes.push_back(index_addr); // Index. + codegen.opcodes.push_back(element_addr); // Destination. + + // Also get type of element. + int element_type_addr = stlevel++; + element_type_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(stlevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::TYPE_OF); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(element_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(element_type_addr); // Address to result. + + // Try the pattern inside the element. + Error err = _parse_match_pattern(codegen, p_pattern->array[i], stlevel, element_addr, element_type_addr, r_bound_variables, r_patch_addresses, element_block_patches); + if (err != OK) { + return err; + } + + // Patch jumps to block to try the next element. + for (int j = 0; j < element_block_patches.size(); j++) { + codegen.opcodes.write[element_block_patches[j]] = codegen.opcodes.size(); + } } - int dst_addr = (p_stack_level) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - codegen.opcodes.push_back(dst_addr); // append the stack level as destination address of the opcode - codegen.alloc_stack(p_stack_level); - return dst_addr; + // Jump to the actual block since it matches. This is needed to take multi-pattern into account. + // Also here for the case of empty arrays. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + r_block_patch_address.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. } break; - //TYPE_TYPE, - default: { - ERR_FAIL_V_MSG(-1, "Bug in bytecode compiler, unexpected node in parse tree while parsing expression."); //unreachable code + case GDScriptParser::PatternNode::PT_DICTIONARY: { + int slevel = p_stack_level; + + // Get dictionary type into constant map. + int dict_type_addr = codegen.get_constant_pos(Variant::DICTIONARY); + + // Check type equality. + int equality_addr = slevel++; + equality_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(slevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(Variant::OP_EQUAL); + codegen.opcodes.push_back(p_type_addr); + codegen.opcodes.push_back(dict_type_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if not the same type. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Store pattern length in constant map. + int dict_length_addr = codegen.get_constant_pos(p_pattern->rest_used ? p_pattern->dictionary.size() - 1 : p_pattern->dictionary.size()); + + // Get user's dictionary length. + int value_length_addr = slevel++; + codegen.alloc_stack(slevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::LEN); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(p_value_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(value_length_addr); // Address to result. + + // Test length compatibility. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_OPERATOR); + codegen.opcodes.push_back(p_pattern->rest_used ? Variant::OP_GREATER_EQUAL : Variant::OP_EQUAL); + codegen.opcodes.push_back(value_length_addr); + codegen.opcodes.push_back(dict_length_addr); + codegen.opcodes.push_back(equality_addr); // Address to result. + + // Jump away if length is not compatible. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(equality_addr); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + // Evaluate element by element. + for (int i = 0; i < p_pattern->dictionary.size(); i++) { + const GDScriptParser::PatternNode::Pair &element = p_pattern->dictionary[i]; + if (element.value_pattern && element.value_pattern->pattern_type == GDScriptParser::PatternNode::PT_REST) { + // Ignore rest pattern. + continue; + } + int stlevel = p_stack_level; + Vector<int> element_block_patches; // I want to internal patterns try the next element instead of going to the block. + + // Get the pattern key. + int pattern_key_addr = _parse_expression(codegen, element.key, stlevel); + if (pattern_key_addr < 0) { + return ERR_PARSE_ERROR; + } + if ((pattern_key_addr >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + stlevel++; + codegen.alloc_stack(stlevel); + } + + // Create stack slot for test result. + int test_result = stlevel++; + test_result |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(stlevel); + + // Check if pattern key exists in user's dictionary. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_RETURN); + codegen.opcodes.push_back(1); // Argument count. + codegen.opcodes.push_back(p_value_addr); // Base (user dictionary). + codegen.opcodes.push_back(codegen.get_name_map_pos("has")); // Function name. + codegen.opcodes.push_back(pattern_key_addr); // Argument (pattern key). + codegen.opcodes.push_back(test_result); // Return address. + + // Jump away if key doesn't exist. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(test_result); + r_patch_addresses.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + + if (element.value_pattern != nullptr) { + // Get actual value from user dictionary. + int value_addr = stlevel++; + codegen.alloc_stack(stlevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_GET); + codegen.opcodes.push_back(p_value_addr); // Source. + codegen.opcodes.push_back(pattern_key_addr); // Index. + codegen.opcodes.push_back(value_addr); // Destination. + + // Also get type of value. + int value_type_addr = stlevel++; + value_type_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(stlevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::TYPE_OF); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(value_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(value_type_addr); // Address to result. + + // Try the pattern inside the value. + Error err = _parse_match_pattern(codegen, element.value_pattern, stlevel, value_addr, value_type_addr, r_bound_variables, r_patch_addresses, element_block_patches); + if (err != OK) { + return err; + } + } + + // Patch jumps to block to try the next element. + for (int j = 0; j < element_block_patches.size(); j++) { + codegen.opcodes.write[element_block_patches[j]] = codegen.opcodes.size(); + } + } + + // Jump to the actual block since it matches. This is needed to take multi-pattern into account. + // Also here for the case of empty dictionaries. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + r_block_patch_address.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. + } break; + case GDScriptParser::PatternNode::PT_REST: + // Do nothing. + break; + case GDScriptParser::PatternNode::PT_WILDCARD: + // This matches anything so just do the jump. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + r_block_patch_address.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be replaced. } + return OK; } -Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::BlockNode *p_block, int p_stack_level, int p_break_addr, int p_continue_addr) { +Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, int p_stack_level, int p_break_addr, int p_continue_addr) { codegen.push_stack_identifiers(); int new_identifiers = 0; - codegen.current_line = p_block->line; + codegen.current_line = p_block->start_line; for (int i = 0; i < p_block->statements.size(); i++) { const GDScriptParser::Node *s = p_block->statements[i]; - switch (s->type) { - case GDScriptParser::Node::TYPE_NEWLINE: { #ifdef DEBUG_ENABLED - const GDScriptParser::NewLineNode *nl = static_cast<const GDScriptParser::NewLineNode *>(s); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); - codegen.opcodes.push_back(nl->line); - codegen.current_line = nl->line; + // Add a newline before each statement, since the debugger needs those. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); + codegen.opcodes.push_back(s->start_line); + codegen.current_line = s->start_line; #endif - } break; - case GDScriptParser::Node::TYPE_CONTROL_FLOW: { - // try subblocks - const GDScriptParser::ControlFlowNode *cf = static_cast<const GDScriptParser::ControlFlowNode *>(s); + switch (s->type) { + case GDScriptParser::Node::MATCH: { + const GDScriptParser::MatchNode *match = static_cast<const GDScriptParser::MatchNode *>(s); - switch (cf->cf_type) { - case GDScriptParser::ControlFlowNode::CF_MATCH: { - GDScriptParser::MatchNode *match = cf->match; + int slevel = p_stack_level; - GDScriptParser::IdentifierNode *id = memnew(GDScriptParser::IdentifierNode); - id->name = "#match_value"; + // First, let's save the addres of the value match. + int temp_addr = _parse_expression(codegen, match->test, slevel); + if (temp_addr < 0) { + return ERR_PARSE_ERROR; + } + if ((temp_addr >> GDScriptFunction::ADDR_BITS & GDScriptFunction::ADDR_TYPE_STACK) == GDScriptFunction::ADDR_TYPE_STACK) { + slevel++; + codegen.alloc_stack(slevel); + } - // var #match_value - // copied because there is no _parse_statement :( - codegen.add_stack_identifier(id->name, p_stack_level++); - codegen.alloc_stack(p_stack_level); - new_identifiers++; + // Then, let's save the type of the value in the stack too, so we can reuse for later comparisons. + int type_addr = slevel++; + type_addr |= GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS; + codegen.alloc_stack(slevel); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_BUILT_IN); + codegen.opcodes.push_back(GDScriptFunctions::TYPE_OF); + codegen.opcodes.push_back(1); // One argument. + codegen.opcodes.push_back(temp_addr); // Argument is the value we want to test. + codegen.opcodes.push_back(type_addr); // Address to result. - GDScriptParser::OperatorNode *op = memnew(GDScriptParser::OperatorNode); - op->op = GDScriptParser::OperatorNode::OP_ASSIGN; - op->arguments.push_back(id); - op->arguments.push_back(match->val_to_match); + Vector<int> patch_match_end; // Will patch the jump to the end of match. - int ret = _parse_expression(codegen, op, p_stack_level); - if (ret < 0) { - memdelete(id); - memdelete(op); - return ERR_PARSE_ERROR; - } + // Now we can actually start testing. + // For each branch. + for (int j = 0; j < match->branches.size(); j++) { + const GDScriptParser::MatchBranchNode *branch = match->branches[j]; - // break address - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(codegen.opcodes.size() + 3); - int break_addr = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(0); // break addr - - for (int j = 0; j < match->compiled_pattern_branches.size(); j++) { - GDScriptParser::MatchNode::CompiledPatternBranch branch = match->compiled_pattern_branches[j]; - - // jump over continue - // jump unconditionally - // continue address - // compile the condition - int ret2 = _parse_expression(codegen, branch.compiled_pattern, p_stack_level); - if (ret2 < 0) { - memdelete(id); - memdelete(op); - return ERR_PARSE_ERROR; - } + int bound_variables = 0; + codegen.push_stack_identifiers(); // Create an extra block around for binds. - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF); - codegen.opcodes.push_back(ret2); - codegen.opcodes.push_back(codegen.opcodes.size() + 3); - int continue_addr = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(0); - - Error err = _parse_block(codegen, branch.body, p_stack_level, p_break_addr, continue_addr); - if (err) { - memdelete(id); - memdelete(op); - return ERR_PARSE_ERROR; +#ifdef DEBUG_ENABLED + // Add a newline before each branch, since the debugger needs those. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); + codegen.opcodes.push_back(s->start_line); + codegen.current_line = s->start_line; +#endif + Vector<int> patch_addrs; // Will patch with end of pattern to jump. + Vector<int> block_patch_addrs; // Will patch with start of block to jump. + + // For each pattern in branch. + for (int k = 0; k < branch->patterns.size(); k++) { + if (k > 0) { + // Patch jumps per pattern to allow for multipattern. If a pattern fails it just tries the next. + for (int l = 0; l < patch_addrs.size(); l++) { + codegen.opcodes.write[patch_addrs[l]] = codegen.opcodes.size(); } - - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(break_addr); - - codegen.opcodes.write[continue_addr + 1] = codegen.opcodes.size(); + patch_addrs.clear(); + } + Error err = _parse_match_pattern(codegen, branch->patterns[k], slevel, temp_addr, type_addr, bound_variables, patch_addrs, block_patch_addrs); + if (err != OK) { + return err; } + } + // Patch jumps to the block. + for (int k = 0; k < block_patch_addrs.size(); k++) { + codegen.opcodes.write[block_patch_addrs[k]] = codegen.opcodes.size(); + } - codegen.opcodes.write[break_addr + 1] = codegen.opcodes.size(); + // Leave space for bound variables. + slevel += bound_variables; + codegen.alloc_stack(slevel); - memdelete(id); - memdelete(op); + // Parse the branch block. + _parse_block(codegen, branch->block, slevel, p_break_addr, p_continue_addr); - } break; + // Jump to end of match. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + patch_match_end.push_back(codegen.opcodes.size()); + codegen.opcodes.push_back(0); // Will be patched. - case GDScriptParser::ControlFlowNode::CF_IF: { - int ret2 = _parse_expression(codegen, cf->arguments[0], p_stack_level, false); - if (ret2 < 0) { - return ERR_PARSE_ERROR; - } + // Patch the addresses of last pattern to jump to the end of the branch, into the next one. + for (int k = 0; k < patch_addrs.size(); k++) { + codegen.opcodes.write[patch_addrs[k]] = codegen.opcodes.size(); + } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); - codegen.opcodes.push_back(ret2); - int else_addr = codegen.opcodes.size(); - codegen.opcodes.push_back(0); //temporary + codegen.pop_stack_identifiers(); // Get out of extra block. + } + // Patch the addresses to jump to the end of the match statement. + for (int j = 0; j < patch_match_end.size(); j++) { + codegen.opcodes.write[patch_match_end[j]] = codegen.opcodes.size(); + } + } break; - Error err = _parse_block(codegen, cf->body, p_stack_level, p_break_addr, p_continue_addr); - if (err) { - return err; - } + case GDScriptParser::Node::IF: { + const GDScriptParser::IfNode *if_n = static_cast<const GDScriptParser::IfNode *>(s); + int ret2 = _parse_expression(codegen, if_n->condition, p_stack_level, false); + if (ret2 < 0) { + return ERR_PARSE_ERROR; + } - if (cf->body_else) { - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - int end_addr = codegen.opcodes.size(); - codegen.opcodes.push_back(0); - codegen.opcodes.write[else_addr] = codegen.opcodes.size(); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(ret2); + int else_addr = codegen.opcodes.size(); + codegen.opcodes.push_back(0); //temporary - codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); - codegen.opcodes.push_back(cf->body_else->line); - codegen.current_line = cf->body_else->line; + Error err = _parse_block(codegen, if_n->true_block, p_stack_level, p_break_addr, p_continue_addr); + if (err) { + return err; + } - Error err2 = _parse_block(codegen, cf->body_else, p_stack_level, p_break_addr, p_continue_addr); - if (err2) { - return err2; - } + if (if_n->false_block) { + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + int end_addr = codegen.opcodes.size(); + codegen.opcodes.push_back(0); + codegen.opcodes.write[else_addr] = codegen.opcodes.size(); - codegen.opcodes.write[end_addr] = codegen.opcodes.size(); - } else { - //end without else - codegen.opcodes.write[else_addr] = codegen.opcodes.size(); - } + Error err2 = _parse_block(codegen, if_n->false_block, p_stack_level, p_break_addr, p_continue_addr); + if (err2) { + return err2; + } - } break; - case GDScriptParser::ControlFlowNode::CF_FOR: { - int slevel = p_stack_level; - int iter_stack_pos = slevel; - int iterator_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - int counter_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - int container_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); - codegen.alloc_stack(slevel); + codegen.opcodes.write[end_addr] = codegen.opcodes.size(); + } else { + //end without else + codegen.opcodes.write[else_addr] = codegen.opcodes.size(); + } - codegen.push_stack_identifiers(); - codegen.add_stack_identifier(static_cast<const GDScriptParser::IdentifierNode *>(cf->arguments[0])->name, iter_stack_pos); + } break; + case GDScriptParser::Node::FOR: { + const GDScriptParser::ForNode *for_n = static_cast<const GDScriptParser::ForNode *>(s); + int slevel = p_stack_level; + int iter_stack_pos = slevel; + int iterator_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + int counter_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + int container_pos = (slevel++) | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS); + codegen.alloc_stack(slevel); - int ret2 = _parse_expression(codegen, cf->arguments[1], slevel, false); - if (ret2 < 0) { - return ERR_COMPILATION_FAILED; - } + codegen.push_stack_identifiers(); + codegen.add_stack_identifier(for_n->variable->name, iter_stack_pos); - //assign container - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); - codegen.opcodes.push_back(container_pos); - codegen.opcodes.push_back(ret2); - - //begin loop - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ITERATE_BEGIN); - codegen.opcodes.push_back(counter_pos); - codegen.opcodes.push_back(container_pos); - codegen.opcodes.push_back(codegen.opcodes.size() + 4); - codegen.opcodes.push_back(iterator_pos); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); //skip code for next - codegen.opcodes.push_back(codegen.opcodes.size() + 8); - //break loop - int break_pos = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); //skip code for next - codegen.opcodes.push_back(0); //skip code for next - //next loop - int continue_pos = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_ITERATE); - codegen.opcodes.push_back(counter_pos); - codegen.opcodes.push_back(container_pos); - codegen.opcodes.push_back(break_pos); - codegen.opcodes.push_back(iterator_pos); - - Error err = _parse_block(codegen, cf->body, slevel, break_pos, continue_pos); - if (err) { - return err; - } + int ret2 = _parse_expression(codegen, for_n->list, slevel, false); + if (ret2 < 0) { + return ERR_COMPILATION_FAILED; + } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(continue_pos); - codegen.opcodes.write[break_pos + 1] = codegen.opcodes.size(); + //assign container + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ASSIGN); + codegen.opcodes.push_back(container_pos); + codegen.opcodes.push_back(ret2); - codegen.pop_stack_identifiers(); + //begin loop + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ITERATE_BEGIN); + codegen.opcodes.push_back(counter_pos); + codegen.opcodes.push_back(container_pos); + codegen.opcodes.push_back(codegen.opcodes.size() + 4); + codegen.opcodes.push_back(iterator_pos); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); //skip code for next + codegen.opcodes.push_back(codegen.opcodes.size() + 8); + //break loop + int break_pos = codegen.opcodes.size(); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); //skip code for next + codegen.opcodes.push_back(0); //skip code for next + //next loop + int continue_pos = codegen.opcodes.size(); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_ITERATE); + codegen.opcodes.push_back(counter_pos); + codegen.opcodes.push_back(container_pos); + codegen.opcodes.push_back(break_pos); + codegen.opcodes.push_back(iterator_pos); + + Error err = _parse_block(codegen, for_n->loop, slevel, break_pos, continue_pos); + if (err) { + return err; + } - } break; - case GDScriptParser::ControlFlowNode::CF_WHILE: { - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(codegen.opcodes.size() + 3); - int break_addr = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(0); - int continue_addr = codegen.opcodes.size(); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(continue_pos); + codegen.opcodes.write[break_pos + 1] = codegen.opcodes.size(); - int ret2 = _parse_expression(codegen, cf->arguments[0], p_stack_level, false); - if (ret2 < 0) { - return ERR_PARSE_ERROR; - } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); - codegen.opcodes.push_back(ret2); - codegen.opcodes.push_back(break_addr); - Error err = _parse_block(codegen, cf->body, p_stack_level, break_addr, continue_addr); - if (err) { - return err; - } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(continue_addr); + codegen.pop_stack_identifiers(); - codegen.opcodes.write[break_addr + 1] = codegen.opcodes.size(); + } break; + case GDScriptParser::Node::WHILE: { + const GDScriptParser::WhileNode *while_n = static_cast<const GDScriptParser::WhileNode *>(s); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(codegen.opcodes.size() + 3); + int break_addr = codegen.opcodes.size(); + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(0); + int continue_addr = codegen.opcodes.size(); + + int ret2 = _parse_expression(codegen, while_n->condition, p_stack_level, false); + if (ret2 < 0) { + return ERR_PARSE_ERROR; + } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_IF_NOT); + codegen.opcodes.push_back(ret2); + codegen.opcodes.push_back(break_addr); + Error err = _parse_block(codegen, while_n->loop, p_stack_level, break_addr, continue_addr); + if (err) { + return err; + } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(continue_addr); - } break; - case GDScriptParser::ControlFlowNode::CF_BREAK: { - if (p_break_addr < 0) { - _set_error("'break'' not within loop", cf); - return ERR_COMPILATION_FAILED; - } - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(p_break_addr); - - } break; - case GDScriptParser::ControlFlowNode::CF_CONTINUE: { - if (p_continue_addr < 0) { - _set_error("'continue' not within loop", cf); - return ERR_COMPILATION_FAILED; - } + codegen.opcodes.write[break_addr + 1] = codegen.opcodes.size(); - codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); - codegen.opcodes.push_back(p_continue_addr); + } break; + case GDScriptParser::Node::BREAK: { + if (p_break_addr < 0) { + _set_error("'break'' not within loop", s); + return ERR_COMPILATION_FAILED; + } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(p_break_addr); - } break; - case GDScriptParser::ControlFlowNode::CF_RETURN: { - int ret2; + } break; + case GDScriptParser::Node::CONTINUE: { + if (p_continue_addr < 0) { + _set_error("'continue' not within loop", s); + return ERR_COMPILATION_FAILED; + } - if (cf->arguments.size()) { - ret2 = _parse_expression(codegen, cf->arguments[0], p_stack_level, false); - if (ret2 < 0) { - return ERR_PARSE_ERROR; - } + codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP); + codegen.opcodes.push_back(p_continue_addr); - } else { - ret2 = GDScriptFunction::ADDR_TYPE_NIL << GDScriptFunction::ADDR_BITS; - } + } break; + case GDScriptParser::Node::RETURN: { + const GDScriptParser::ReturnNode *return_n = static_cast<const GDScriptParser::ReturnNode *>(s); + int ret2; - codegen.opcodes.push_back(GDScriptFunction::OPCODE_RETURN); - codegen.opcodes.push_back(ret2); + if (return_n->return_value != nullptr) { + ret2 = _parse_expression(codegen, return_n->return_value, p_stack_level, false); + if (ret2 < 0) { + return ERR_PARSE_ERROR; + } - } break; + } else { + ret2 = GDScriptFunction::ADDR_TYPE_NIL << GDScriptFunction::ADDR_BITS; } + + codegen.opcodes.push_back(GDScriptFunction::OPCODE_RETURN); + codegen.opcodes.push_back(ret2); + } break; - case GDScriptParser::Node::TYPE_ASSERT: { + case GDScriptParser::Node::ASSERT: { #ifdef DEBUG_ENABLED // try subblocks @@ -1578,14 +2054,14 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Blo codegen.opcodes.push_back(message_ret); #endif } break; - case GDScriptParser::Node::TYPE_BREAKPOINT: { + case GDScriptParser::Node::BREAKPOINT: { #ifdef DEBUG_ENABLED // try subblocks codegen.opcodes.push_back(GDScriptFunction::OPCODE_BREAKPOINT); #endif } break; - case GDScriptParser::Node::TYPE_LOCAL_VAR: { - const GDScriptParser::LocalVarNode *lv = static_cast<const GDScriptParser::LocalVarNode *>(s); + case GDScriptParser::Node::VARIABLE: { + const GDScriptParser::VariableNode *lv = static_cast<const GDScriptParser::VariableNode *>(s); // since we are using properties now for most class access, allow shadowing of class members to make user's life easier. // @@ -1594,20 +2070,49 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Blo // return ERR_ALREADY_EXISTS; //} - codegen.add_stack_identifier(lv->name, p_stack_level++); + codegen.add_stack_identifier(lv->identifier->name, p_stack_level++); codegen.alloc_stack(p_stack_level); new_identifiers++; + if (lv->initializer != nullptr) { + int dst_address = codegen.stack_identifiers[lv->identifier->name]; + dst_address |= GDScriptFunction::ADDR_TYPE_STACK_VARIABLE << GDScriptFunction::ADDR_BITS; + + int src_address = _parse_expression(codegen, lv->initializer, p_stack_level); + if (src_address < 0) { + return ERR_PARSE_ERROR; + } + if (!_generate_typed_assign(codegen, src_address, dst_address, _gdtype_from_datatype(lv->get_datatype()), lv->initializer->get_datatype())) { + return ERR_PARSE_ERROR; + } + } } break; + case GDScriptParser::Node::CONSTANT: { + // Local constants. + const GDScriptParser::ConstantNode *lc = static_cast<const GDScriptParser::ConstantNode *>(s); + if (!lc->initializer->is_constant) { + _set_error("Local constant must have a constant value as initializer.", lc->initializer); + return ERR_PARSE_ERROR; + } + codegen.local_named_constants[lc->identifier->name] = codegen.get_constant_pos(lc->initializer->reduced_value); + } break; + case GDScriptParser::Node::PASS: + // Nothing to do. + break; default: { //expression - int ret2 = _parse_expression(codegen, s, p_stack_level, true); - if (ret2 < 0) { - return ERR_PARSE_ERROR; + if (s->is_expression()) { + int ret2 = _parse_expression(codegen, static_cast<const GDScriptParser::ExpressionNode *>(s), p_stack_level, true); + if (ret2 < 0) { + return ERR_PARSE_ERROR; + } + } else { + ERR_FAIL_V_MSG(ERR_INVALID_DATA, "Bug in bytecode compiler, unexpected node in parse tree while parsing statement."); //unreachable code } } break; } } + codegen.pop_stack_identifiers(); return OK; } @@ -1626,9 +2131,10 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser Vector<StringName> argnames; int stack_level = 0; + int optional_parameters = 0; if (p_func) { - for (int i = 0; i < p_func->arguments.size(); i++) { + for (int i = 0; i < p_func->parameters.size(); i++) { // since we are using properties now for most class access, allow shadowing of class members to make user's life easier. // //if (_is_class_member_property(p_script, p_func->arguments[i])) { @@ -1636,42 +2142,81 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser // return ERR_ALREADY_EXISTS; //} - codegen.add_stack_identifier(p_func->arguments[i], i); + codegen.add_stack_identifier(p_func->parameters[i]->identifier->name, i); #ifdef TOOLS_ENABLED - argnames.push_back(p_func->arguments[i]); + argnames.push_back(p_func->parameters[i]->identifier->name); #endif + if (p_func->parameters[i]->default_value != nullptr) { + optional_parameters++; + } } - stack_level = p_func->arguments.size(); + stack_level = p_func->parameters.size(); } codegen.alloc_stack(stack_level); /* Parse initializer -if applies- */ - bool is_initializer = !p_for_ready && !p_func; + bool is_implicit_initializer = !p_for_ready && !p_func; + bool is_initializer = p_func && String(p_func->identifier->name) == GDScriptLanguage::get_singleton()->strings._init; - if (is_initializer || (p_func && String(p_func->name) == "_init")) { - //parse initializer for class members - if (!p_func && p_class->extends_used && p_script->native.is_null()) { - //call implicit parent constructor - codegen.opcodes.push_back(GDScriptFunction::OPCODE_CALL_SELF_BASE); - codegen.opcodes.push_back(codegen.get_name_map_pos("_init")); - codegen.opcodes.push_back(0); - codegen.opcodes.push_back((GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS) | 0); - } - Error err = _parse_block(codegen, p_class->initializer, stack_level); - if (err) { - return err; + if (is_implicit_initializer) { + // Initialize class fields. + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type != GDScriptParser::ClassNode::Member::VARIABLE) { + continue; + } + const GDScriptParser::VariableNode *field = p_class->members[i].variable; + if (field->onready) { + // Only initialize in _ready. + continue; + } + + if (field->initializer) { + // Emit proper line change. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); + codegen.opcodes.push_back(field->initializer->start_line); + + int src_address = _parse_expression(codegen, field->initializer, stack_level, false, true); + if (src_address < 0) { + return ERR_PARSE_ERROR; + } + int dst_address = codegen.script->member_indices[field->identifier->name].index; + dst_address |= GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS; + + if (!_generate_typed_assign(codegen, src_address, dst_address, _gdtype_from_datatype(field->get_datatype()), field->initializer->get_datatype())) { + return ERR_PARSE_ERROR; + } + } } - is_initializer = true; } - if (p_for_ready || (p_func && String(p_func->name) == "_ready")) { - //parse initializer for class members - if (p_class->ready->statements.size()) { - Error err = _parse_block(codegen, p_class->ready, stack_level); - if (err) { - return err; + if (p_for_ready || (p_func && String(p_func->identifier->name) == "_ready")) { + // Initialize class fields on ready. + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type != GDScriptParser::ClassNode::Member::VARIABLE) { + continue; + } + const GDScriptParser::VariableNode *field = p_class->members[i].variable; + if (!field->onready) { + continue; + } + + if (field->initializer) { + // Emit proper line change. + codegen.opcodes.push_back(GDScriptFunction::OPCODE_LINE); + codegen.opcodes.push_back(field->initializer->start_line); + + int src_address = _parse_expression(codegen, field->initializer, stack_level, false, true); + if (src_address < 0) { + return ERR_PARSE_ERROR; + } + int dst_address = codegen.script->member_indices[field->identifier->name].index; + dst_address |= GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS; + + if (!_generate_typed_assign(codegen, src_address, dst_address, _gdtype_from_datatype(field->get_datatype()), field->initializer->get_datatype())) { + return ERR_PARSE_ERROR; + } } } } @@ -1682,31 +2227,39 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser StringName func_name; if (p_func) { - if (p_func->default_values.size()) { + if (optional_parameters > 0) { codegen.opcodes.push_back(GDScriptFunction::OPCODE_JUMP_TO_DEF_ARGUMENT); defarg_addr.push_back(codegen.opcodes.size()); - for (int i = 0; i < p_func->default_values.size(); i++) { - _parse_expression(codegen, p_func->default_values[i], stack_level, true); + for (int i = p_func->parameters.size() - optional_parameters; i < p_func->parameters.size(); i++) { + int src_addr = _parse_expression(codegen, p_func->parameters[i]->default_value, stack_level, true); + if (src_addr < 0) { + return ERR_PARSE_ERROR; + } + int dst_addr = codegen.stack_identifiers[p_func->parameters[i]->identifier->name] | (GDScriptFunction::ADDR_TYPE_STACK_VARIABLE << GDScriptFunction::ADDR_BITS); + if (!_generate_typed_assign(codegen, src_addr, dst_addr, _gdtype_from_datatype(p_func->parameters[i]->get_datatype()), p_func->parameters[i]->default_value->get_datatype())) { + return ERR_PARSE_ERROR; + } defarg_addr.push_back(codegen.opcodes.size()); } - defarg_addr.invert(); } + func_name = p_func->identifier->name; + codegen.function_name = func_name; Error err = _parse_block(codegen, p_func->body, stack_level); if (err) { return err; } - func_name = p_func->name; } else { if (p_for_ready) { func_name = "_ready"; } else { - func_name = "_init"; + func_name = "@implicit_new"; } } + codegen.function_name = func_name; codegen.opcodes.push_back(GDScriptFunction::OPCODE_END); /* @@ -1719,13 +2272,13 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser //} if (p_func) { - gdfunc->_static = p_func->_static; + gdfunc->_static = p_func->is_static; gdfunc->rpc_mode = p_func->rpc_mode; - gdfunc->argument_types.resize(p_func->argument_types.size()); - for (int i = 0; i < p_func->argument_types.size(); i++) { - gdfunc->argument_types.write[i] = _gdtype_from_datatype(p_func->argument_types[i]); + gdfunc->argument_types.resize(p_func->parameters.size()); + for (int i = 0; i < p_func->parameters.size(); i++) { + gdfunc->argument_types.write[i] = _gdtype_from_datatype(p_func->parameters[i]->get_datatype()); } - gdfunc->return_type = _gdtype_from_datatype(p_func->return_type); + gdfunc->return_type = _gdtype_from_datatype(p_func->get_datatype()); } else { gdfunc->_static = false; gdfunc->rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; @@ -1797,7 +2350,7 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser gdfunc->_default_arg_ptr = nullptr; } - gdfunc->_argument_count = p_func ? p_func->arguments.size() : 0; + gdfunc->_argument_count = p_func ? p_func->parameters.size() : 0; gdfunc->_stack_size = codegen.stack_max; gdfunc->_call_size = codegen.call_max; gdfunc->name = func_name; @@ -1810,15 +2363,15 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser } //loc if (p_func) { - signature += "::" + itos(p_func->body->line); + signature += "::" + itos(p_func->body->start_line); } else { signature += "::0"; } //function and class - if (p_class->name) { - signature += "::" + String(p_class->name) + "." + String(func_name); + if (p_class->identifier) { + signature += "::" + String(p_class->identifier->name) + "." + String(func_name); } else { signature += "::" + String(func_name); } @@ -1838,10 +2391,10 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser #endif if (p_func) { - gdfunc->_initial_line = p_func->line; + gdfunc->_initial_line = p_func->start_line; #ifdef TOOLS_ENABLED - p_script->member_lines[func_name] = p_func->line; + p_script->member_lines[func_name] = p_func->start_line; #endif } else { gdfunc->_initial_line = 0; @@ -1854,6 +2407,153 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser if (is_initializer) { p_script->initializer = gdfunc; } + if (is_implicit_initializer) { + p_script->implicit_initializer = gdfunc; + } + + return OK; +} + +Error GDScriptCompiler::_parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter) { + Vector<int> bytecode; + CodeGen codegen; + + codegen.class_node = p_class; + codegen.script = p_script; + codegen.function_node = nullptr; + codegen.stack_max = 0; + codegen.current_line = 0; + codegen.call_max = 0; + codegen.debug_stack = EngineDebugger::is_active(); + Vector<StringName> argnames; + + int stack_level = 0; + + if (p_is_setter) { + codegen.add_stack_identifier(p_variable->setter_parameter->name, stack_level++); + argnames.push_back(p_variable->setter_parameter->name); + } + + codegen.alloc_stack(stack_level); + + StringName func_name; + + if (p_is_setter) { + func_name = "@" + p_variable->identifier->name + "_setter"; + } else { + func_name = "@" + p_variable->identifier->name + "_getter"; + } + codegen.function_name = func_name; + + Error err = _parse_block(codegen, p_is_setter ? p_variable->setter : p_variable->getter, stack_level); + if (err != OK) { + return err; + } + + codegen.opcodes.push_back(GDScriptFunction::OPCODE_END); + + p_script->member_functions[func_name] = memnew(GDScriptFunction); + GDScriptFunction *gdfunc = p_script->member_functions[func_name]; + + gdfunc->_static = false; + gdfunc->rpc_mode = p_variable->rpc_mode; + gdfunc->argument_types.resize(p_is_setter ? 1 : 0); + gdfunc->return_type = _gdtype_from_datatype(p_variable->get_datatype()); +#ifdef TOOLS_ENABLED + gdfunc->arg_names = argnames; +#endif + + // TODO: Unify this with function compiler. + //constants + if (codegen.constant_map.size()) { + gdfunc->_constant_count = codegen.constant_map.size(); + gdfunc->constants.resize(codegen.constant_map.size()); + gdfunc->_constants_ptr = gdfunc->constants.ptrw(); + const Variant *K = nullptr; + while ((K = codegen.constant_map.next(K))) { + int idx = codegen.constant_map[*K]; + gdfunc->constants.write[idx] = *K; + } + } else { + gdfunc->_constants_ptr = nullptr; + gdfunc->_constant_count = 0; + } + //global names + if (codegen.name_map.size()) { + gdfunc->global_names.resize(codegen.name_map.size()); + gdfunc->_global_names_ptr = &gdfunc->global_names[0]; + for (Map<StringName, int>::Element *E = codegen.name_map.front(); E; E = E->next()) { + gdfunc->global_names.write[E->get()] = E->key(); + } + gdfunc->_global_names_count = gdfunc->global_names.size(); + + } else { + gdfunc->_global_names_ptr = nullptr; + gdfunc->_global_names_count = 0; + } + +#ifdef TOOLS_ENABLED + // Named globals + if (codegen.named_globals.size()) { + gdfunc->named_globals.resize(codegen.named_globals.size()); + gdfunc->_named_globals_ptr = gdfunc->named_globals.ptr(); + for (int i = 0; i < codegen.named_globals.size(); i++) { + gdfunc->named_globals.write[i] = codegen.named_globals[i]; + } + gdfunc->_named_globals_count = gdfunc->named_globals.size(); + } +#endif + + gdfunc->code = codegen.opcodes; + gdfunc->_code_ptr = &gdfunc->code[0]; + gdfunc->_code_size = codegen.opcodes.size(); + gdfunc->_default_arg_count = 0; + gdfunc->_default_arg_ptr = nullptr; + gdfunc->_argument_count = argnames.size(); + gdfunc->_stack_size = codegen.stack_max; + gdfunc->_call_size = codegen.call_max; + gdfunc->name = func_name; +#ifdef DEBUG_ENABLED + if (EngineDebugger::is_active()) { + String signature; + //path + if (p_script->get_path() != String()) { + signature += p_script->get_path(); + } + //loc + signature += "::" + itos(p_is_setter ? p_variable->setter->start_line : p_variable->getter->start_line); + + //function and class + + if (p_class->identifier) { + signature += "::" + String(p_class->identifier->name) + "." + String(func_name); + } else { + signature += "::" + String(func_name); + } + + gdfunc->profile.signature = signature; + } +#endif + gdfunc->_script = p_script; + gdfunc->source = source; + +#ifdef DEBUG_ENABLED + + { + gdfunc->func_cname = (String(source) + " - " + String(func_name)).utf8(); + gdfunc->_func_cname = gdfunc->func_cname.get_data(); + } + +#endif + gdfunc->_initial_line = p_is_setter ? p_variable->setter->start_line : p_variable->getter->start_line; +#ifdef TOOLS_ENABLED + + p_script->member_lines[func_name] = gdfunc->_initial_line; +#endif + + if (codegen.debug_stack) { + gdfunc->stack_debug = codegen.stack_debug; + } return OK; } @@ -1861,14 +2561,14 @@ Error GDScriptCompiler::_parse_function(GDScript *p_script, const GDScriptParser Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state) { parsing_classes.insert(p_script); - if (p_class->owner && p_class->owner->owner) { + if (p_class->outer && p_class->outer->outer) { // Owner is not root if (!parsed_classes.has(p_script->_owner)) { if (parsing_classes.has(p_script->_owner)) { - _set_error("Cyclic class reference for '" + String(p_class->name) + "'.", p_class); + _set_error("Cyclic class reference for '" + String(p_class->identifier->name) + "'.", p_class); return ERR_PARSE_ERROR; } - Error err = _parse_class_level(p_script->_owner, p_class->owner, p_keep_state); + Error err = _parse_class_level(p_script->_owner, p_class->outer, p_keep_state); if (err) { return err; } @@ -1889,8 +2589,8 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar p_script->_signals.clear(); p_script->initializer = nullptr; - p_script->tool = p_class->tool; - p_script->name = p_class->name; + p_script->tool = parser->is_tool(); + p_script->name = p_class->identifier ? p_class->identifier->name : ""; Ref<GDScriptNativeClass> native; @@ -1907,12 +2607,12 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar Ref<GDScript> base = base_type.script_type; p_script->base = base; p_script->_base = base.ptr(); - p_script->member_indices = base->member_indices; - if (p_class->base_type.kind == GDScriptParser::DataType::CLASS) { + if (p_class->base_type.kind == GDScriptParser::DataType::CLASS && p_class->base_type.class_type != nullptr) { if (!parsed_classes.has(p_script->_base)) { if (parsing_classes.has(p_script->_base)) { - _set_error("Cyclic class reference for '" + String(p_class->name) + "'.", p_class); + String class_name = p_class->identifier ? p_class->identifier->name : "<main>"; + _set_error("Cyclic class reference for '" + class_name + "'.", p_class); return ERR_PARSE_ERROR; } Error err = _parse_class_level(p_script->_base, p_class->base_type.class_type, p_keep_state); @@ -1921,6 +2621,8 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar } } } + + p_script->member_indices = base->member_indices; } break; default: { _set_error("Parser bug: invalid inheritance.", p_class); @@ -1928,86 +2630,146 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar } break; } - for (int i = 0; i < p_class->variables.size(); i++) { - StringName name = p_class->variables[i].identifier; - - GDScript::MemberInfo minfo; - minfo.index = p_script->member_indices.size(); - minfo.setter = p_class->variables[i].setter; - minfo.getter = p_class->variables[i].getter; - minfo.rpc_mode = p_class->variables[i].rpc_mode; - minfo.data_type = _gdtype_from_datatype(p_class->variables[i].data_type); + for (int i = 0; i < p_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = p_class->members[i]; + switch (member.type) { + case GDScriptParser::ClassNode::Member::VARIABLE: { + const GDScriptParser::VariableNode *variable = member.variable; + StringName name = variable->identifier->name; + + GDScript::MemberInfo minfo; + minfo.index = p_script->member_indices.size(); + switch (variable->property) { + case GDScriptParser::VariableNode::PROP_NONE: + break; // Nothing to do. + case GDScriptParser::VariableNode::PROP_SETGET: + if (variable->setter_pointer != nullptr) { + minfo.setter = variable->setter_pointer->name; + } + if (variable->getter_pointer != nullptr) { + minfo.getter = variable->getter_pointer->name; + } + break; + case GDScriptParser::VariableNode::PROP_INLINE: + if (variable->setter != nullptr) { + minfo.setter = "@" + variable->identifier->name + "_setter"; + } + if (variable->getter != nullptr) { + minfo.getter = "@" + variable->identifier->name + "_getter"; + } + break; + } + minfo.rpc_mode = variable->rpc_mode; + minfo.data_type = _gdtype_from_datatype(variable->get_datatype()); - PropertyInfo prop_info = minfo.data_type; - prop_info.name = name; - PropertyInfo export_info = p_class->variables[i]._export; + PropertyInfo prop_info = minfo.data_type; + prop_info.name = name; + PropertyInfo export_info = variable->export_info; - if (export_info.type != Variant::NIL) { - if (!minfo.data_type.has_type) { - prop_info.type = export_info.type; - prop_info.class_name = export_info.class_name; - } - prop_info.hint = export_info.hint; - prop_info.hint_string = export_info.hint_string; - prop_info.usage = export_info.usage; + if (variable->exported) { + if (!minfo.data_type.has_type) { + prop_info.type = export_info.type; + prop_info.class_name = export_info.class_name; + } + prop_info.hint = export_info.hint; + prop_info.hint_string = export_info.hint_string; + prop_info.usage = export_info.usage; #ifdef TOOLS_ENABLED - if (p_class->variables[i].default_value.get_type() != Variant::NIL) { - p_script->member_default_values[name] = p_class->variables[i].default_value; - } + if (variable->initializer != nullptr && variable->initializer->type == GDScriptParser::Node::LITERAL) { + p_script->member_default_values[name] = static_cast<const GDScriptParser::LiteralNode *>(variable->initializer)->value; + } #endif - } else { - prop_info.usage = PROPERTY_USAGE_SCRIPT_VARIABLE; - } + } else { + prop_info.usage = PROPERTY_USAGE_SCRIPT_VARIABLE; + } - p_script->member_info[name] = prop_info; - p_script->member_indices[name] = minfo; - p_script->members.insert(name); + p_script->member_info[name] = prop_info; + p_script->member_indices[name] = minfo; + p_script->members.insert(name); #ifdef TOOLS_ENABLED - p_script->member_lines[name] = p_class->variables[i].line; + p_script->member_lines[name] = variable->start_line; #endif - } + } break; - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { - StringName name = E->key(); + case GDScriptParser::ClassNode::Member::CONSTANT: { + const GDScriptParser::ConstantNode *constant = member.constant; + StringName name = constant->identifier->name; - ERR_CONTINUE(E->get().expression->type != GDScriptParser::Node::TYPE_CONSTANT); + ERR_CONTINUE(constant->initializer->type != GDScriptParser::Node::LITERAL); - GDScriptParser::ConstantNode *constant = static_cast<GDScriptParser::ConstantNode *>(E->get().expression); + const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(constant->initializer); - p_script->constants.insert(name, constant->value); + p_script->constants.insert(name, literal->value); #ifdef TOOLS_ENABLED - p_script->member_lines[name] = E->get().expression->line; + p_script->member_lines[name] = constant->start_line; #endif - } + } break; - for (int i = 0; i < p_class->_signals.size(); i++) { - StringName name = p_class->_signals[i].name; + case GDScriptParser::ClassNode::Member::ENUM_VALUE: { + const GDScriptParser::EnumNode::Value &enum_value = member.enum_value; + StringName name = enum_value.identifier->name; - GDScript *c = p_script; + p_script->constants.insert(name, enum_value.value); +#ifdef TOOLS_ENABLED + p_script->member_lines[name] = enum_value.identifier->start_line; +#endif + } break; - while (c) { - if (c->_signals.has(name)) { - _set_error("Signal '" + name + "' redefined (in current or parent class)", p_class); - return ERR_ALREADY_EXISTS; - } + case GDScriptParser::ClassNode::Member::SIGNAL: { + const GDScriptParser::SignalNode *signal = member.signal; + StringName name = signal->identifier->name; - if (c->base.is_valid()) { - c = c->base.ptr(); - } else { - c = nullptr; - } - } + GDScript *c = p_script; - if (native.is_valid()) { - if (ClassDB::has_signal(native->get_name(), name)) { - _set_error("Signal '" + name + "' redefined (original in native class '" + String(native->get_name()) + "')", p_class); - return ERR_ALREADY_EXISTS; - } - } + while (c) { + if (c->_signals.has(name)) { + _set_error("Signal '" + name + "' redefined (in current or parent class)", p_class); + return ERR_ALREADY_EXISTS; + } + + if (c->base.is_valid()) { + c = c->base.ptr(); + } else { + c = nullptr; + } + } + + if (native.is_valid()) { + if (ClassDB::has_signal(native->get_name(), name)) { + _set_error("Signal '" + name + "' redefined (original in native class '" + String(native->get_name()) + "')", p_class); + return ERR_ALREADY_EXISTS; + } + } + + Vector<StringName> parameters_names; + parameters_names.resize(signal->parameters.size()); + for (int j = 0; j < signal->parameters.size(); j++) { + parameters_names.write[j] = signal->parameters[j]->identifier->name; + } + p_script->_signals[name] = parameters_names; + } break; + + case GDScriptParser::ClassNode::Member::ENUM: { + const GDScriptParser::EnumNode *enum_n = member.m_enum; - p_script->_signals[name] = p_class->_signals[i].arguments; + // TODO: Make enums not be just a dictionary? + Dictionary new_enum; + for (int j = 0; j < enum_n->values.size(); j++) { + int value = enum_n->values[j].value; + // Needs to be string because Variant::get will convert to String. + new_enum[String(enum_n->values[j].identifier->name)] = value; + } + + p_script->constants.insert(enum_n->identifier->name, new_enum); +#ifdef TOOLS_ENABLED + p_script->member_lines[enum_n->identifier->name] = enum_n->start_line; +#endif + } break; + default: + break; // Nothing to do here. + } } parsed_classes.insert(p_script); @@ -2015,14 +2777,19 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar //parse sub-classes - for (int i = 0; i < p_class->subclasses.size(); i++) { - StringName name = p_class->subclasses[i]->name; + for (int i = 0; i < p_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = p_class->members[i]; + if (member.type != member.CLASS) { + continue; + } + const GDScriptParser::ClassNode *inner_class = member.m_class; + StringName name = inner_class->identifier->name; Ref<GDScript> &subclass = p_script->subclasses[name]; GDScript *subclass_ptr = subclass.ptr(); // Subclass might still be parsing, just skip it if (!parsed_classes.has(subclass_ptr) && !parsing_classes.has(subclass_ptr)) { - Error err = _parse_class_level(subclass_ptr, p_class->subclasses[i], p_keep_state); + Error err = _parse_class_level(subclass_ptr, inner_class, p_keep_state); if (err) { return err; } @@ -2030,7 +2797,7 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar #ifdef TOOLS_ENABLED - p_script->member_lines[name] = p_class->subclasses[i]->line; + p_script->member_lines[name] = inner_class->start_line; #endif p_script->constants.insert(name, subclass); //once parsed, goes to the list of constants @@ -2042,41 +2809,48 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar Error GDScriptCompiler::_parse_class_blocks(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state) { //parse methods - bool has_initializer = false; bool has_ready = false; - for (int i = 0; i < p_class->functions.size(); i++) { - if (!has_initializer && p_class->functions[i]->name == "_init") { - has_initializer = true; - } - if (!has_ready && p_class->functions[i]->name == "_ready") { - has_ready = true; - } - Error err = _parse_function(p_script, p_class, p_class->functions[i]); - if (err) { - return err; - } - } - - //parse static methods - - for (int i = 0; i < p_class->static_functions.size(); i++) { - Error err = _parse_function(p_script, p_class, p_class->static_functions[i]); - if (err) { - return err; + for (int i = 0; i < p_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = p_class->members[i]; + if (member.type == member.FUNCTION) { + const GDScriptParser::FunctionNode *function = member.function; + if (!has_ready && function->identifier->name == "_ready") { + has_ready = true; + } + Error err = _parse_function(p_script, p_class, function); + if (err) { + return err; + } + } else if (member.type == member.VARIABLE) { + const GDScriptParser::VariableNode *variable = member.variable; + if (variable->property == GDScriptParser::VariableNode::PROP_INLINE) { + if (variable->setter != nullptr) { + Error err = _parse_setter_getter(p_script, p_class, variable, true); + if (err) { + return err; + } + } + if (variable->getter != nullptr) { + Error err = _parse_setter_getter(p_script, p_class, variable, false); + if (err) { + return err; + } + } + } } } - if (!has_initializer) { - //create a constructor + { + // Create an implicit constructor in any case. Error err = _parse_function(p_script, p_class, nullptr); if (err) { return err; } } - if (!has_ready && p_class->ready->statements.size()) { - //create a constructor + if (!has_ready && p_class->onready_used) { + //create a _ready constructor Error err = _parse_function(p_script, p_class, nullptr, true); if (err) { return err; @@ -2132,11 +2906,15 @@ Error GDScriptCompiler::_parse_class_blocks(GDScript *p_script, const GDScriptPa } #endif - for (int i = 0; i < p_class->subclasses.size(); i++) { - StringName name = p_class->subclasses[i]->name; + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type != GDScriptParser::ClassNode::Member::CLASS) { + continue; + } + const GDScriptParser::ClassNode *inner_class = p_class->members[i].m_class; + StringName name = inner_class->identifier->name; GDScript *subclass = p_script->subclasses[name].ptr(); - Error err = _parse_class_blocks(subclass, p_class->subclasses[i], p_keep_state); + Error err = _parse_class_blocks(subclass, inner_class, p_keep_state); if (err) { return err; } @@ -2155,8 +2933,12 @@ void GDScriptCompiler::_make_scripts(GDScript *p_script, const GDScriptParser::C p_script->subclasses.clear(); - for (int i = 0; i < p_class->subclasses.size(); i++) { - StringName name = p_class->subclasses[i]->name; + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type != GDScriptParser::ClassNode::Member::CLASS) { + continue; + } + const GDScriptParser::ClassNode *inner_class = p_class->members[i].m_class; + StringName name = inner_class->identifier->name; Ref<GDScript> subclass; String fully_qualified_name = p_script->fully_qualified_name + "::" + name; @@ -2176,7 +2958,7 @@ void GDScriptCompiler::_make_scripts(GDScript *p_script, const GDScriptParser::C subclass->fully_qualified_name = fully_qualified_name; p_script->subclasses.insert(name, subclass); - _make_scripts(subclass.ptr(), p_class->subclasses[i], false); + _make_scripts(subclass.ptr(), inner_class, false); } } @@ -2186,8 +2968,7 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri error = ""; parser = p_parser; main_script = p_script; - const GDScriptParser::Node *root = parser->get_parse_tree(); - ERR_FAIL_COND_V(root->type != GDScriptParser::Node::TYPE_CLASS, ERR_INVALID_DATA); + const GDScriptParser::ClassNode *root = parser->get_tree(); source = p_script->get_path(); @@ -2195,22 +2976,22 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri p_script->fully_qualified_name = p_script->path; // Create scripts for subclasses beforehand so they can be referenced - _make_scripts(p_script, static_cast<const GDScriptParser::ClassNode *>(root), p_keep_state); + _make_scripts(p_script, root, p_keep_state); p_script->_owner = nullptr; - Error err = _parse_class_level(p_script, static_cast<const GDScriptParser::ClassNode *>(root), p_keep_state); + Error err = _parse_class_level(p_script, root, p_keep_state); if (err) { return err; } - err = _parse_class_blocks(p_script, static_cast<const GDScriptParser::ClassNode *>(root), p_keep_state); + err = _parse_class_blocks(p_script, root, p_keep_state); if (err) { return err; } - return OK; + return GDScriptCache::finish_compiling(p_script->get_path()); } String GDScriptCompiler::get_error() const { diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h index 315d4f1842..e8601f69c7 100644 --- a/modules/gdscript/gdscript_compiler.h +++ b/modules/gdscript/gdscript_compiler.h @@ -33,6 +33,7 @@ #include "core/set.h" #include "gdscript.h" +#include "gdscript_function.h" #include "gdscript_parser.h" class GDScriptCompiler { @@ -44,6 +45,7 @@ class GDScriptCompiler { GDScript *script; const GDScriptParser::ClassNode *class_node; const GDScriptParser::FunctionNode *function_node; + StringName function_name; bool debug_stack; List<Map<StringName, int>> stack_id_stack; @@ -52,6 +54,7 @@ class GDScriptCompiler { List<GDScriptFunction::StackDebug> stack_debug; List<Map<StringName, int>> block_identifier_stack; Map<StringName, int> block_identifiers; + Map<StringName, int> local_named_constants; void add_stack_identifier(const StringName &p_id, int p_stackpos) { stack_identifiers[p_id] = p_stackpos; @@ -111,11 +114,11 @@ class GDScriptCompiler { int get_constant_pos(const Variant &p_constant) { if (constant_map.has(p_constant)) { - return constant_map[p_constant]; + return constant_map[p_constant] | (GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS); } int pos = constant_map.size(); constant_map[p_constant] = pos; - return pos; + return pos | (GDScriptFunction::ADDR_TYPE_LOCAL_CONSTANT << GDScriptFunction::ADDR_BITS); } Vector<int> opcodes; @@ -140,15 +143,19 @@ class GDScriptCompiler { void _set_error(const String &p_error, const GDScriptParser::Node *p_node); - bool _create_unary_operator(CodeGen &codegen, const GDScriptParser::OperatorNode *on, Variant::Operator op, int p_stack_level); - bool _create_binary_operator(CodeGen &codegen, const GDScriptParser::OperatorNode *on, Variant::Operator op, int p_stack_level, bool p_initializer = false, int p_index_addr = 0); + bool _create_unary_operator(CodeGen &codegen, const GDScriptParser::UnaryOpNode *on, Variant::Operator op, int p_stack_level); + bool _create_binary_operator(CodeGen &codegen, const GDScriptParser::BinaryOpNode *on, Variant::Operator op, int p_stack_level, bool p_initializer = false, int p_index_addr = 0); + bool _create_binary_operator(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_left_operand, const GDScriptParser::ExpressionNode *p_right_operand, Variant::Operator op, int p_stack_level, bool p_initializer = false, int p_index_addr = 0); + bool _generate_typed_assign(CodeGen &codegen, int p_src_address, int p_dst_address, const GDScriptDataType &p_datatype, const GDScriptParser::DataType &p_value_type); GDScriptDataType _gdtype_from_datatype(const GDScriptParser::DataType &p_datatype) const; - int _parse_assign_right_expression(CodeGen &codegen, const GDScriptParser::OperatorNode *p_expression, int p_stack_level, int p_index_addr = 0); - int _parse_expression(CodeGen &codegen, const GDScriptParser::Node *p_expression, int p_stack_level, bool p_root = false, bool p_initializer = false, int p_index_addr = 0); - Error _parse_block(CodeGen &codegen, const GDScriptParser::BlockNode *p_block, int p_stack_level = 0, int p_break_addr = -1, int p_continue_addr = -1); + int _parse_assign_right_expression(CodeGen &codegen, const GDScriptParser::AssignmentNode *p_assignment, int p_stack_level, int p_index_addr = 0); + int _parse_expression(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_expression, int p_stack_level, bool p_root = false, bool p_initializer = false, int p_index_addr = 0); + Error _parse_match_pattern(CodeGen &codegen, const GDScriptParser::PatternNode *p_pattern, int p_stack_level, int p_value_addr, int p_type_addr, int &r_bound_variables, Vector<int> &r_patch_addresses, Vector<int> &r_block_patch_address); + Error _parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, int p_stack_level = 0, int p_break_addr = -1, int p_continue_addr = -1); Error _parse_function(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::FunctionNode *p_func, bool p_for_ready = false); + Error _parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter); Error _parse_class_level(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); Error _parse_class_blocks(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); void _make_scripts(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); @@ -156,6 +163,7 @@ class GDScriptCompiler { int err_column; StringName source; String error; + bool within_await = false; public: Error compile(const GDScriptParser *p_parser, GDScript *p_script, bool p_keep_state = false); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 3a5db3687b..88bf8cde0f 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -33,9 +33,13 @@ #include "core/engine.h" #include "core/global_constants.h" #include "core/os/file_access.h" +#include "gdscript_analyzer.h" #include "gdscript_compiler.h" +#include "gdscript_parser.h" +#include "gdscript_tokenizer.h" #ifdef TOOLS_ENABLED +#include "core/project_settings.h" #include "editor/editor_file_system.h" #include "editor/editor_settings.h" #endif @@ -114,16 +118,35 @@ void GDScriptLanguage::make_template(const String &p_class_name, const String &p p_script->set_source_code(_template); } +static void get_function_names_recursively(const GDScriptParser::ClassNode *p_class, const String &p_prefix, Map<int, String> &r_funcs) { + for (int i = 0; i < p_class->members.size(); i++) { + if (p_class->members[i].type == GDScriptParser::ClassNode::Member::FUNCTION) { + const GDScriptParser::FunctionNode *function = p_class->members[i].function; + r_funcs[function->start_line] = p_prefix.empty() ? String(function->identifier->name) : p_prefix + "." + String(function->identifier->name); + } else if (p_class->members[i].type == GDScriptParser::ClassNode::Member::CLASS) { + String new_prefix = p_class->members[i].m_class->identifier->name; + get_function_names_recursively(p_class->members[i].m_class, p_prefix.empty() ? new_prefix : p_prefix + "." + new_prefix, r_funcs); + } + } +} + bool GDScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings, Set<int> *r_safe_lines) const { GDScriptParser parser; + GDScriptAnalyzer analyzer(&parser); - Error err = parser.parse(p_script, p_path.get_base_dir(), true, p_path, false, r_safe_lines); + Error err = parser.parse(p_script, p_path, false); + if (err == OK) { + err = analyzer.analyze(); + } #ifdef DEBUG_ENABLED if (r_warnings) { for (const List<GDScriptWarning>::Element *E = parser.get_warnings().front(); E; E = E->next()) { const GDScriptWarning &warn = E->get(); ScriptLanguage::Warning w; - w.line = warn.line; + w.start_line = warn.start_line; + w.end_line = warn.end_line; + w.leftmost_column = warn.leftmost_column; + w.rightmost_column = warn.rightmost_column; w.code = (int)warn.code; w.string_code = GDScriptWarning::get_name_from_code(warn.code); w.message = warn.get_message(); @@ -132,38 +155,33 @@ bool GDScriptLanguage::validate(const String &p_script, int &r_line_error, int & } #endif if (err) { - r_line_error = parser.get_error_line(); - r_col_error = parser.get_error_column(); - r_test_error = parser.get_error(); + GDScriptParser::ParserError parse_error = parser.get_errors().front()->get(); + r_line_error = parse_error.line; + r_col_error = parse_error.column; + r_test_error = parse_error.message; return false; } else { - const GDScriptParser::Node *root = parser.get_parse_tree(); - ERR_FAIL_COND_V(root->type != GDScriptParser::Node::TYPE_CLASS, false); - - const GDScriptParser::ClassNode *cl = static_cast<const GDScriptParser::ClassNode *>(root); + const GDScriptParser::ClassNode *cl = parser.get_tree(); Map<int, String> funcs; - for (int i = 0; i < cl->functions.size(); i++) { - funcs[cl->functions[i]->line] = cl->functions[i]->name; - } - - for (int i = 0; i < cl->static_functions.size(); i++) { - funcs[cl->static_functions[i]->line] = cl->static_functions[i]->name; - } - for (int i = 0; i < cl->subclasses.size(); i++) { - for (int j = 0; j < cl->subclasses[i]->functions.size(); j++) { - funcs[cl->subclasses[i]->functions[j]->line] = String(cl->subclasses[i]->name) + "." + cl->subclasses[i]->functions[j]->name; - } - for (int j = 0; j < cl->subclasses[i]->static_functions.size(); j++) { - funcs[cl->subclasses[i]->static_functions[j]->line] = String(cl->subclasses[i]->name) + "." + cl->subclasses[i]->static_functions[j]->name; - } - } + get_function_names_recursively(cl, "", funcs); for (Map<int, String>::Element *E = funcs.front(); E; E = E->next()) { r_functions->push_back(E->get() + ":" + itos(E->key())); } } +#ifdef DEBUG_ENABLED + if (r_safe_lines) { + const Set<int> &unsafe_lines = parser.get_unsafe_lines(); + for (int i = 1; i <= parser.get_last_line_number(); i++) { + if (!unsafe_lines.has(i)) { + r_safe_lines->insert(i); + } + } + } +#endif + return true; } @@ -176,20 +194,26 @@ bool GDScriptLanguage::supports_builtin_mode() const { } int GDScriptLanguage::find_function(const String &p_function, const String &p_code) const { - GDScriptTokenizerText tokenizer; - tokenizer.set_code(p_code); + GDScriptTokenizer tokenizer; + tokenizer.set_source_code(p_code); int indent = 0; - while (tokenizer.get_token() != GDScriptTokenizer::TK_EOF && tokenizer.get_token() != GDScriptTokenizer::TK_ERROR) { - if (tokenizer.get_token() == GDScriptTokenizer::TK_NEWLINE) { - indent = tokenizer.get_token_line_indent(); - } - if (indent == 0 && tokenizer.get_token() == GDScriptTokenizer::TK_PR_FUNCTION && tokenizer.get_token(1) == GDScriptTokenizer::TK_IDENTIFIER) { - String identifier = tokenizer.get_token_identifier(1); - if (identifier == p_function) { - return tokenizer.get_token_line(); + GDScriptTokenizer::Token current = tokenizer.scan(); + while (current.type != GDScriptTokenizer::Token::TK_EOF && current.type != GDScriptTokenizer::Token::ERROR) { + if (current.type == GDScriptTokenizer::Token::INDENT) { + indent++; + } else if (current.type == GDScriptTokenizer::Token::DEDENT) { + indent--; + } + if (indent == 0 && current.type == GDScriptTokenizer::Token::FUNC) { + current = tokenizer.scan(); + if (current.is_identifier()) { + String identifier = current.get_identifier(); + if (identifier == p_function) { + return current.start_line; + } } } - tokenizer.advance(); + current = tokenizer.scan(); } return -1; } @@ -397,16 +421,6 @@ void GDScriptLanguage::get_public_functions(List<MethodInfo> *p_functions) const } { MethodInfo mi; - mi.name = "yield"; - mi.arguments.push_back(PropertyInfo(Variant::OBJECT, "object")); - mi.arguments.push_back(PropertyInfo(Variant::STRING, "signal")); - mi.default_arguments.push_back(Variant()); - mi.default_arguments.push_back(String()); - mi.return_val = PropertyInfo(Variant::OBJECT, "", PROPERTY_HINT_RESOURCE_TYPE, "GDScriptFunctionState"); - p_functions->push_back(mi); - } - { - MethodInfo mi; mi.name = "assert"; mi.return_val.type = Variant::NIL; mi.arguments.push_back(PropertyInfo(Variant::BOOL, "condition")); @@ -467,44 +481,46 @@ String GDScriptLanguage::make_function(const String &p_class, const String &p_na //////// COMPLETION ////////// -#if defined(DEBUG_METHODS_ENABLED) && defined(TOOLS_ENABLED) - -struct GDScriptCompletionContext { - const GDScriptParser::ClassNode *_class = nullptr; - const GDScriptParser::FunctionNode *function = nullptr; - const GDScriptParser::BlockNode *block = nullptr; - Object *base = nullptr; - String base_path; - int line = 0; - uint32_t depth = 0; - - GDScriptCompletionContext() {} -}; +#ifdef TOOLS_ENABLED struct GDScriptCompletionIdentifier { GDScriptParser::DataType type; String enumeration; Variant value; - const GDScriptParser::Node *assigned_expression = nullptr; - - GDScriptCompletionIdentifier() {} + const GDScriptParser::ExpressionNode *assigned_expression = nullptr; }; -static void _get_directory_contents(EditorFileSystemDirectory *p_dir, Map<String, ScriptCodeCompletionOption> &r_list) { - const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", false) ? "'" : "\""; - - for (int i = 0; i < p_dir->get_file_count(); i++) { - ScriptCodeCompletionOption option(p_dir->get_file_path(i), ScriptCodeCompletionOption::KIND_FILE_PATH); - option.insert_text = quote_style + option.display + quote_style; - r_list.insert(option.display, option); - } - - for (int i = 0; i < p_dir->get_subdir_count(); i++) { - _get_directory_contents(p_dir->get_subdir(i), r_list); +// TODO: Move this to a central location (maybe core?). +static const char *underscore_classes[] = { + "ClassDB", + "Directory", + "Engine", + "File", + "Geometry", + "GodotSharp", + "JSON", + "Marshalls", + "Mutex", + "OS", + "ResourceLoader", + "ResourceSaver", + "Semaphore", + "Thread", + "VisualScriptEditor", + nullptr, +}; +static StringName _get_real_class_name(const StringName &p_source) { + const char **class_name = underscore_classes; + while (*class_name != nullptr) { + if (p_source == *class_name) { + return String("_") + p_source; + } + class_name++; } + return p_source; } -static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = true) { +static String _get_visual_datatype(const PropertyInfo &p_info, bool p_is_arg = true) { if (p_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) { String enum_name = p_info.class_name; if (enum_name.find(".") == -1) { @@ -527,7 +543,7 @@ static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = tr } } if (p_info.type == Variant::NIL) { - if (p_isarg || (p_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT)) { + if (p_is_arg || (p_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT)) { return "Variant"; } else { return "void"; @@ -537,11 +553,538 @@ static String _get_visual_datatype(const PropertyInfo &p_info, bool p_isarg = tr return Variant::get_type_name(p_info.type); } +static String _make_arguments_hint(const MethodInfo &p_info, int p_arg_idx, bool p_is_annotation = false) { + String arghint; + if (!p_is_annotation) { + arghint += _get_visual_datatype(p_info.return_val, false) + " "; + } + arghint += p_info.name + "("; + + int def_args = p_info.arguments.size() - p_info.default_arguments.size(); + int i = 0; + for (const List<PropertyInfo>::Element *E = p_info.arguments.front(); E; E = E->next()) { + if (i > 0) { + arghint += ", "; + } + + if (i == p_arg_idx) { + arghint += String::chr(0xFFFF); + } + arghint += E->get().name + ": " + _get_visual_datatype(E->get(), true); + + if (i - def_args >= 0) { + arghint += String(" = ") + p_info.default_arguments[i - def_args].get_construct_string(); + } + + if (i == p_arg_idx) { + arghint += String::chr(0xFFFF); + } + + i++; + } + + if (p_info.flags & METHOD_FLAG_VARARG) { + if (p_info.arguments.size() > 0) { + arghint += ", "; + } + if (p_arg_idx >= p_info.arguments.size()) { + arghint += String::chr(0xFFFF); + } + arghint += "..."; + if (p_arg_idx >= p_info.arguments.size()) { + arghint += String::chr(0xFFFF); + } + } + + arghint += ")"; + + return arghint; +} + +static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_function, int p_arg_idx) { + String arghint = p_function->get_datatype().to_string() + " " + p_function->identifier->name.operator String() + "("; + + for (int i = 0; i < p_function->parameters.size(); i++) { + if (i > 0) { + arghint += ", "; + } + + if (i == p_arg_idx) { + arghint += String::chr(0xFFFF); + } + const GDScriptParser::ParameterNode *par = p_function->parameters[i]; + arghint += par->identifier->name.operator String() + ": " + par->get_datatype().to_string(); + + if (par->default_value) { + String def_val = "<unknown>"; + if (par->default_value->type == GDScriptParser::Node::LITERAL) { + const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(par->default_value); + def_val = literal->value.get_construct_string(); + } else if (par->default_value->type == GDScriptParser::Node::IDENTIFIER) { + const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(par->default_value); + def_val = id->name.operator String(); + } + arghint += " = " + def_val; + } + if (i == p_arg_idx) { + arghint += String::chr(0xFFFF); + } + } + + arghint += ")"; + + return arghint; +} + +static void _get_directory_contents(EditorFileSystemDirectory *p_dir, Map<String, ScriptCodeCompletionOption> &r_list) { + const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", false) ? "'" : "\""; + + for (int i = 0; i < p_dir->get_file_count(); i++) { + ScriptCodeCompletionOption option(p_dir->get_file_path(i), ScriptCodeCompletionOption::KIND_FILE_PATH); + option.insert_text = quote_style + option.display + quote_style; + r_list.insert(option.display, option); + } + + for (int i = 0; i < p_dir->get_subdir_count(); i++) { + _get_directory_contents(p_dir->get_subdir(i), r_list); + } +} + +static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_annotation, int p_argument, const String p_quote_style, Map<String, ScriptCodeCompletionOption> &r_result) { + if (p_annotation->name == "@export_range" || p_annotation->name == "@export_exp_range") { + if (p_argument == 3 || p_argument == 4) { + // Slider hint. + ScriptCodeCompletionOption slider1("or_greater", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + slider1.insert_text = p_quote_style + slider1.display + p_quote_style; + r_result.insert(slider1.display, slider1); + ScriptCodeCompletionOption slider2("or_lesser", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + slider2.insert_text = p_quote_style + slider2.display + p_quote_style; + r_result.insert(slider2.display, slider2); + } + } else if (p_annotation->name == "@export_exp_easing") { + if (p_argument == 0 || p_argument == 1) { + // Easing hint. + ScriptCodeCompletionOption hint1("attenuation", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + hint1.insert_text = p_quote_style + hint1.display + p_quote_style; + r_result.insert(hint1.display, hint1); + ScriptCodeCompletionOption hint2("inout", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + hint2.insert_text = p_quote_style + hint2.display + p_quote_style; + r_result.insert(hint2.display, hint2); + } + } else if (p_annotation->name == "@export_node_path") { + ScriptCodeCompletionOption node("Node", ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(node.display, node); + List<StringName> node_types; + ClassDB::get_inheriters_from_class("Node", &node_types); + for (const List<StringName>::Element *E = node_types.front(); E != nullptr; E = E->next()) { + if (!ClassDB::is_class_exposed(E->get())) { + continue; + } + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } + } +} + +static void _list_available_types(bool p_inherit_only, GDScriptParser::CompletionContext &p_context, Map<String, ScriptCodeCompletionOption> &r_result) { + List<StringName> native_types; + ClassDB::get_class_list(&native_types); + for (const List<StringName>::Element *E = native_types.front(); E != nullptr; E = E->next()) { + if (ClassDB::is_class_exposed(E->get()) && !Engine::get_singleton()->has_singleton(E->get())) { + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } + } + + if (p_context.current_class) { + if (!p_inherit_only && p_context.current_class->base_type.is_set()) { + // Native enums from base class + List<StringName> enums; + ClassDB::get_enum_list(p_context.current_class->base_type.native_type, &enums); + for (const List<StringName>::Element *E = enums.front(); E != nullptr; E = E->next()) { + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_ENUM); + r_result.insert(option.display, option); + } + } + // Check current class for potential types + const GDScriptParser::ClassNode *current = p_context.current_class; + while (current) { + for (int i = 0; i < current->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = current->members[i]; + switch (member.type) { + case GDScriptParser::ClassNode::Member::CLASS: { + ScriptCodeCompletionOption option(member.m_class->identifier->name, ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } break; + case GDScriptParser::ClassNode::Member::ENUM: { + if (!p_inherit_only) { + ScriptCodeCompletionOption option(member.m_enum->identifier->name, ScriptCodeCompletionOption::KIND_ENUM); + r_result.insert(option.display, option); + } + } break; + case GDScriptParser::ClassNode::Member::CONSTANT: { + if (member.constant->get_datatype().is_meta_type && p_context.current_class->outer != nullptr) { + ScriptCodeCompletionOption option(member.constant->identifier->name, ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } + } break; + default: + break; + } + } + current = current->outer; + } + } + + // Global scripts + List<StringName> global_classes; + ScriptServer::get_global_class_list(&global_classes); + for (const List<StringName>::Element *E = global_classes.front(); E != nullptr; E = E->next()) { + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } + + // Autoload singletons + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); + for (const Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E != nullptr; E = E->next()) { + const ProjectSettings::AutoloadInfo &info = E->get(); + if (!info.is_singleton || info.path.get_extension().to_lower() != "gd") { + continue; + } + ScriptCodeCompletionOption option(info.name, ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } +} + +static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, Map<String, ScriptCodeCompletionOption> &r_result) { + for (int i = 0; i < p_suite->locals.size(); i++) { + ScriptCodeCompletionOption option(p_suite->locals[i].name, ScriptCodeCompletionOption::KIND_VARIABLE); + r_result.insert(option.display, option); + } + if (p_suite->parent_block) { + _find_identifiers_in_suite(p_suite->parent_block, r_result); + } +} + +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result); + +static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, bool p_only_functions, bool p_static, bool p_parent_only, Map<String, ScriptCodeCompletionOption> &r_result) { + if (!p_parent_only) { + bool outer = false; + const GDScriptParser::ClassNode *clss = p_class; + while (clss) { + for (int i = 0; i < clss->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = clss->members[i]; + ScriptCodeCompletionOption option; + switch (member.type) { + case GDScriptParser::ClassNode::Member::VARIABLE: + if (p_only_functions || outer || (p_static)) { + continue; + } + option = ScriptCodeCompletionOption(member.variable->identifier->name, ScriptCodeCompletionOption::KIND_MEMBER); + break; + case GDScriptParser::ClassNode::Member::CONSTANT: + if (p_only_functions) { + continue; + } + option = ScriptCodeCompletionOption(member.constant->identifier->name, ScriptCodeCompletionOption::KIND_CONSTANT); + break; + case GDScriptParser::ClassNode::Member::CLASS: + if (p_only_functions) { + continue; + } + option = ScriptCodeCompletionOption(member.m_class->identifier->name, ScriptCodeCompletionOption::KIND_CLASS); + break; + case GDScriptParser::ClassNode::Member::ENUM_VALUE: + if (p_only_functions) { + continue; + } + option = ScriptCodeCompletionOption(member.enum_value.identifier->name, ScriptCodeCompletionOption::KIND_CONSTANT); + break; + case GDScriptParser::ClassNode::Member::ENUM: + if (p_only_functions) { + continue; + } + option = ScriptCodeCompletionOption(member.m_enum->identifier->name, ScriptCodeCompletionOption::KIND_ENUM); + break; + case GDScriptParser::ClassNode::Member::FUNCTION: + if (outer || (p_static && !member.function->is_static) || member.function->identifier->name.operator String().begins_with("@")) { + continue; + } + option = ScriptCodeCompletionOption(member.function->identifier->name, ScriptCodeCompletionOption::KIND_FUNCTION); + if (member.function->parameters.size() > 0) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + break; + case GDScriptParser::ClassNode::Member::SIGNAL: + if (p_only_functions || outer) { + clss = clss->outer; + continue; + } + option = ScriptCodeCompletionOption(member.signal->identifier->name, ScriptCodeCompletionOption::KIND_SIGNAL); + break; + case GDScriptParser::ClassNode::Member::UNDEFINED: + break; + } + r_result.insert(option.display, option); + } + outer = true; + clss = clss->outer; + } + } + + // Parents. + GDScriptCompletionIdentifier base_type; + base_type.type = p_class->base_type; + base_type.type.is_meta_type = p_static; + + _find_identifiers_in_base(base_type, p_only_functions, r_result); +} + +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { + GDScriptParser::DataType base_type = p_base.type; + bool _static = base_type.is_meta_type; + + if (_static && base_type.kind != GDScriptParser::DataType::BUILTIN) { + ScriptCodeCompletionOption option("new", ScriptCodeCompletionOption::KIND_FUNCTION); + option.insert_text += "("; + r_result.insert(option.display, option); + } + + while (!base_type.has_no_type()) { + switch (base_type.kind) { + case GDScriptParser::DataType::CLASS: { + _find_identifiers_in_class(base_type.class_type, p_only_functions, _static, false, r_result); + // This already finds all parent identifiers, so we are done. + base_type = GDScriptParser::DataType(); + } break; + case GDScriptParser::DataType::SCRIPT: { + Ref<Script> scr = base_type.script_type; + if (scr.is_valid()) { + if (!p_only_functions) { + if (!_static) { + List<PropertyInfo> members; + scr->get_script_property_list(&members); + for (List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); + r_result.insert(option.display, option); + } + } + Map<StringName, Variant> constants; + scr->get_constants(&constants); + for (Map<StringName, Variant>::Element *E = constants.front(); E; E = E->next()) { + ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CONSTANT); + r_result.insert(option.display, option); + } + + List<MethodInfo> signals; + scr->get_script_signal_list(&signals); + for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_SIGNAL); + r_result.insert(option.display, option); + } + } + + List<MethodInfo> methods; + scr->get_script_method_list(&methods); + for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { + if (E->get().name.begins_with("@")) { + continue; + } + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); + if (E->get().arguments.size()) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + r_result.insert(option.display, option); + } + + Ref<Script> base_script = scr->get_base_script(); + if (base_script.is_valid()) { + base_type.script_type = base_script; + } else { + base_type.kind = GDScriptParser::DataType::NATIVE; + base_type.native_type = scr->get_instance_base_type(); + } + } else { + return; + } + } break; + case GDScriptParser::DataType::NATIVE: { + StringName type = _get_real_class_name(base_type.native_type); + if (!ClassDB::class_exists(type)) { + return; + } + + if (!p_only_functions) { + List<String> constants; + ClassDB::get_integer_constant_list(type, &constants); + for (List<String>::Element *E = constants.front(); E; E = E->next()) { + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CONSTANT); + r_result.insert(option.display, option); + } + + if (!_static || Engine::get_singleton()->has_singleton(type)) { + List<PropertyInfo> pinfo; + ClassDB::get_property_list(type, &pinfo); + for (List<PropertyInfo>::Element *E = pinfo.front(); E; E = E->next()) { + if (E->get().usage & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY)) { + continue; + } + if (E->get().name.find("/") != -1) { + continue; + } + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); + r_result.insert(option.display, option); + } + } + } + + if (!_static || Engine::get_singleton()->has_singleton(type)) { + List<MethodInfo> methods; + bool is_autocompleting_getters = GLOBAL_GET("debug/gdscript/completion/autocomplete_setters_and_getters").booleanize(); + ClassDB::get_method_list(type, &methods, false, !is_autocompleting_getters); + for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { + if (E->get().name.begins_with("_")) { + continue; + } + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); + if (E->get().arguments.size()) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + r_result.insert(option.display, option); + } + } + + return; + } break; + case GDScriptParser::DataType::BUILTIN: { + Callable::CallError err; + Variant tmp = Variant::construct(base_type.builtin_type, nullptr, 0, err); + if (err.error != Callable::CallError::CALL_OK) { + return; + } + + if (!p_only_functions) { + List<PropertyInfo> members; + if (p_base.value.get_type() != Variant::NIL) { + p_base.value.get_property_list(&members); + } else { + tmp.get_property_list(&members); + } + + for (List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { + if (String(E->get().name).find("/") == -1) { + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); + r_result.insert(option.display, option); + } + } + } + + List<MethodInfo> methods; + tmp.get_method_list(&methods); + for (const List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { + ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); + if (E->get().arguments.size()) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + r_result.insert(option.display, option); + } + + return; + } break; + default: { + return; + } break; + } + } +} + +static void _find_identifiers(GDScriptParser::CompletionContext &p_context, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { + if (!p_only_functions && p_context.current_suite) { + // This includes function parameters, since they are also locals. + _find_identifiers_in_suite(p_context.current_suite, r_result); + } + + if (p_context.current_class) { + _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result); + } + + for (int i = 0; i < GDScriptFunctions::FUNC_MAX; i++) { + MethodInfo function = GDScriptFunctions::get_info(GDScriptFunctions::Function(i)); + ScriptCodeCompletionOption option(String(GDScriptFunctions::get_func_name(GDScriptFunctions::Function(i))), ScriptCodeCompletionOption::KIND_FUNCTION); + if (function.arguments.size() || (function.flags & METHOD_FLAG_VARARG)) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + r_result.insert(option.display, option); + } + + if (p_only_functions) { + return; + } + + static const char *_type_names[Variant::VARIANT_MAX] = { + "null", "bool", "int", "float", "String", "StringName", "Vector2", "Vector2i", "Rect2", "Rect2i", "Vector3", "Vector3i", "Transform2D", "Plane", "Quat", "AABB", "Basis", "Transform", + "Color", "NodePath", "RID", "Signal", "Callable", "Object", "Dictionary", "Array", "PackedByteArray", "PackedInt32Array", "PackedInt64Array", "PackedFloat32Array", "PackedFloat64Array", "PackedStringArray", + "PackedVector2Array", "PackedVector3Array", "PackedColorArray" + }; + static_assert((sizeof(_type_names) / sizeof(*_type_names)) == Variant::VARIANT_MAX, "Completion for builtin types is incomplete"); + + for (int i = 0; i < Variant::VARIANT_MAX; i++) { + ScriptCodeCompletionOption option(_type_names[i], ScriptCodeCompletionOption::KIND_CLASS); + r_result.insert(option.display, option); + } + + static const char *_keywords[] = { + "and", "in", "not", "or", "false", "PI", "TAU", "INF", "NAN", "self", "true", "as", "assert", + "breakpoint", "class", "extends", "is", "func", "preload", "signal", "tool", "await", + "const", "enum", "static", "super", "var", "break", "continue", "if", "elif", + "else", "for", "pass", "return", "match", "while", + 0 + }; + + const char **kw = _keywords; + while (*kw) { + ScriptCodeCompletionOption option(*kw, ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + r_result.insert(option.display, option); + kw++; + } + + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); + for (const Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E != nullptr; E = E->next()) { + if (!E->value().is_singleton) { + continue; + } + ScriptCodeCompletionOption option(E->key(), ScriptCodeCompletionOption::KIND_CONSTANT); + r_result.insert(option.display, option); + } + + // Native classes and global constants. + for (const Map<StringName, int>::Element *E = GDScriptLanguage::get_singleton()->get_global_map().front(); E; E = E->next()) { + ScriptCodeCompletionOption option; + if (ClassDB::class_exists(E->key()) || Engine::get_singleton()->has_singleton(E->key())) { + option = ScriptCodeCompletionOption(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); + } else { + option = ScriptCodeCompletionOption(E->key().operator String(), ScriptCodeCompletionOption::KIND_CONSTANT); + } + r_result.insert(option.display, option); + } +} + static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) { GDScriptCompletionIdentifier ci; ci.value = p_value; ci.type.is_constant = true; - ci.type.has_type = true; + ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; ci.type.kind = GDScriptParser::DataType::BUILTIN; ci.type.builtin_type = p_value.get_type(); @@ -560,12 +1103,7 @@ static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) { } if (scr.is_valid()) { ci.type.script_type = scr; - Ref<GDScript> gds = scr; - if (gds.is_valid()) { - ci.type.kind = GDScriptParser::DataType::GDSCRIPT; - } else { - ci.type.kind = GDScriptParser::DataType::SCRIPT; - } + ci.type.kind = GDScriptParser::DataType::SCRIPT; ci.type.native_type = scr->get_instance_base_type(); } else { ci.type.kind = GDScriptParser::DataType::NATIVE; @@ -587,7 +1125,7 @@ static GDScriptCompletionIdentifier _type_from_property(const PropertyInfo &p_pr ci.enumeration = p_property.class_name; } - ci.type.has_type = true; + ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; ci.type.builtin_type = p_property.type; if (p_property.type == Variant::OBJECT) { ci.type.kind = GDScriptParser::DataType::NATIVE; @@ -598,344 +1136,292 @@ static GDScriptCompletionIdentifier _type_from_property(const PropertyInfo &p_pr return ci; } -static GDScriptCompletionIdentifier _type_from_gdtype(const GDScriptDataType &p_gdtype) { - GDScriptCompletionIdentifier ci; - if (!p_gdtype.has_type) { - return ci; - } +static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type); +static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type); +static bool _guess_method_return_type_from_base(GDScriptParser::CompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, GDScriptCompletionIdentifier &r_type); - ci.type.has_type = true; - ci.type.builtin_type = p_gdtype.builtin_type; - ci.type.native_type = p_gdtype.native_type; - ci.type.script_type = p_gdtype.script_type; - - switch (p_gdtype.kind) { - case GDScriptDataType::UNINITIALIZED: { - ERR_PRINT("Uninitialized completion. Please report a bug."); - } break; - case GDScriptDataType::BUILTIN: { - ci.type.kind = GDScriptParser::DataType::BUILTIN; - } break; - case GDScriptDataType::NATIVE: { - ci.type.kind = GDScriptParser::DataType::NATIVE; - } break; - case GDScriptDataType::GDSCRIPT: { - ci.type.kind = GDScriptParser::DataType::GDSCRIPT; - } break; - case GDScriptDataType::SCRIPT: { - ci.type.kind = GDScriptParser::DataType::SCRIPT; - } break; - } - return ci; -} - -static bool _guess_identifier_type(GDScriptCompletionContext &p_context, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type); -static bool _guess_identifier_type_from_base(GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type); -static bool _guess_method_return_type_from_base(GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, GDScriptCompletionIdentifier &r_type); - -static bool _guess_expression_type(GDScriptCompletionContext &p_context, const GDScriptParser::Node *p_expression, GDScriptCompletionIdentifier &r_type) { +static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, const GDScriptParser::ExpressionNode *p_expression, GDScriptCompletionIdentifier &r_type) { bool found = false; - if (++p_context.depth > 100) { - print_error("Maximum _guess_expression_type depth limit reached. Please file a bugreport."); - return false; - } - - switch (p_expression->type) { - case GDScriptParser::Node::TYPE_CONSTANT: { - const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(p_expression); - r_type = _type_from_variant(cn->value); - found = true; - } break; - case GDScriptParser::Node::TYPE_SELF: { - if (p_context._class) { - r_type.type.has_type = true; - r_type.type.kind = GDScriptParser::DataType::CLASS; - r_type.type.class_type = const_cast<GDScriptParser::ClassNode *>(p_context._class); - r_type.type.is_constant = true; - r_type.value = p_context.base; + if (p_expression->is_constant) { + // Already has a value, so just use that. + r_type = _type_from_variant(p_expression->reduced_value); + found = true; + } else { + switch (p_expression->type) { + case GDScriptParser::Node::LITERAL: { + const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(p_expression); + r_type = _type_from_variant(literal->value); found = true; - } - } break; - case GDScriptParser::Node::TYPE_IDENTIFIER: { - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(p_expression); - found = _guess_identifier_type(p_context, id->name, r_type); - } break; - case GDScriptParser::Node::TYPE_DICTIONARY: { - // Try to recreate the dictionary - const GDScriptParser::DictionaryNode *dn = static_cast<const GDScriptParser::DictionaryNode *>(p_expression); - Dictionary d; - bool full = true; - for (int i = 0; i < dn->elements.size(); i++) { - GDScriptCompletionIdentifier key; - if (_guess_expression_type(p_context, dn->elements[i].key, key)) { - GDScriptCompletionIdentifier value; - if (_guess_expression_type(p_context, dn->elements[i].value, value)) { - if (!value.type.is_constant) { + } break; + case GDScriptParser::Node::SELF: { + if (p_context.current_class) { + r_type.type.kind = GDScriptParser::DataType::CLASS; + r_type.type.type_source = GDScriptParser::DataType::INFERRED; + r_type.type.is_constant = true; + r_type.type.class_type = p_context.current_class; + r_type.value = p_context.base; + found = true; + } + } break; + case GDScriptParser::Node::IDENTIFIER: { + const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(p_expression); + found = _guess_identifier_type(p_context, id->name, r_type); + } break; + case GDScriptParser::Node::DICTIONARY: { + // Try to recreate the dictionary. + const GDScriptParser::DictionaryNode *dn = static_cast<const GDScriptParser::DictionaryNode *>(p_expression); + Dictionary d; + bool full = true; + for (int i = 0; i < dn->elements.size(); i++) { + GDScriptCompletionIdentifier key; + if (_guess_expression_type(p_context, dn->elements[i].key, key)) { + if (!key.type.is_constant) { + full = false; + break; + } + GDScriptCompletionIdentifier value; + if (_guess_expression_type(p_context, dn->elements[i].value, value)) { + if (!value.type.is_constant) { + full = false; + break; + } + d[key.value] = value.value; + } else { full = false; break; } - d[key.value] = value.value; } else { full = false; break; } - } else { - full = false; - break; } - } - if (full) { - // If not fully constant, setting this value is detrimental to the inference - r_type.value = d; - r_type.type.is_constant = true; - } - r_type.type.has_type = true; - r_type.type.kind = GDScriptParser::DataType::BUILTIN; - r_type.type.builtin_type = Variant::DICTIONARY; - } break; - case GDScriptParser::Node::TYPE_ARRAY: { - // Try to recreate the array - const GDScriptParser::ArrayNode *an = static_cast<const GDScriptParser::ArrayNode *>(p_expression); - Array a; - bool full = true; - a.resize(an->elements.size()); - for (int i = 0; i < an->elements.size(); i++) { - GDScriptCompletionIdentifier value; - if (_guess_expression_type(p_context, an->elements[i], value)) { - a[i] = value.value; - } else { - full = false; - break; + if (full) { + r_type.value = d; + r_type.type.is_constant = true; } - } - if (full) { - // If not fully constant, setting this value is detrimental to the inference - r_type.value = a; - } - r_type.type.has_type = true; - r_type.type.kind = GDScriptParser::DataType::BUILTIN; - r_type.type.builtin_type = Variant::ARRAY; - } break; - case GDScriptParser::Node::TYPE_CAST: { - const GDScriptParser::CastNode *cn = static_cast<const GDScriptParser::CastNode *>(p_expression); - GDScriptCompletionIdentifier value; - if (_guess_expression_type(p_context, cn->source_node, r_type)) { - r_type.type = cn->get_datatype(); + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::BUILTIN; + r_type.type.builtin_type = Variant::DICTIONARY; found = true; - } - } break; - case GDScriptParser::Node::TYPE_OPERATOR: { - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(p_expression); - switch (op->op) { - case GDScriptParser::OperatorNode::OP_CALL: { - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_TYPE) { - const GDScriptParser::TypeNode *tn = static_cast<const GDScriptParser::TypeNode *>(op->arguments[0]); - r_type.type.has_type = true; - r_type.type.kind = GDScriptParser::DataType::BUILTIN; - r_type.type.builtin_type = tn->vtype; - found = true; - break; - } else if (op->arguments[0]->type == GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION) { - const GDScriptParser::BuiltInFunctionNode *bin = static_cast<const GDScriptParser::BuiltInFunctionNode *>(op->arguments[0]); - MethodInfo mi = GDScriptFunctions::get_info(bin->function); - r_type = _type_from_property(mi.return_val); - found = true; + } break; + case GDScriptParser::Node::ARRAY: { + // Try to recreate the array + const GDScriptParser::ArrayNode *an = static_cast<const GDScriptParser::ArrayNode *>(p_expression); + Array a; + bool full = true; + a.resize(an->elements.size()); + for (int i = 0; i < an->elements.size(); i++) { + GDScriptCompletionIdentifier value; + if (_guess_expression_type(p_context, an->elements[i], value)) { + if (value.type.is_constant) { + a[i] = value.value; + } else { + full = false; + break; + } + } else { + full = false; break; - } else if (op->arguments.size() >= 2 && op->arguments[1]->type == GDScriptParser::Node::TYPE_IDENTIFIER) { - StringName id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[1])->name; + } + } + if (full) { + // If not fully constant, setting this value is detrimental to the inference. + r_type.value = a; + } + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::BUILTIN; + r_type.type.builtin_type = Variant::ARRAY; + found = true; + } break; + case GDScriptParser::Node::CAST: { + const GDScriptParser::CastNode *cn = static_cast<const GDScriptParser::CastNode *>(p_expression); + GDScriptCompletionIdentifier value; + if (_guess_expression_type(p_context, cn->operand, r_type)) { + r_type.type = cn->get_datatype(); + found = true; + } + } break; + case GDScriptParser::Node::CALL: { + const GDScriptParser::CallNode *call = static_cast<const GDScriptParser::CallNode *>(p_expression); + if (GDScriptParser::get_builtin_type(call->function_name) < Variant::VARIANT_MAX) { + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::BUILTIN; + r_type.type.builtin_type = GDScriptParser::get_builtin_type(call->function_name); + found = true; + break; + } else if (GDScriptParser::get_builtin_function(call->function_name) < GDScriptFunctions::FUNC_MAX) { + MethodInfo mi = GDScriptFunctions::get_info(GDScriptParser::get_builtin_function(call->function_name)); + r_type = _type_from_property(mi.return_val); + found = true; + break; + } else { + GDScriptParser::CompletionContext c = p_context; + c.current_line = call->start_line; - GDScriptCompletionContext c = p_context; - c.line = op->line; + GDScriptParser::Node::Type callee_type = call->get_callee_type(); - GDScriptCompletionIdentifier base; - if (!_guess_expression_type(c, op->arguments[0], base)) { + GDScriptCompletionIdentifier base; + if (callee_type == GDScriptParser::Node::IDENTIFIER || call->is_super) { + // Simple call, so base is 'self'. + if (p_context.current_class) { + base.type.kind = GDScriptParser::DataType::CLASS; + base.type.type_source = GDScriptParser::DataType::INFERRED; + base.type.is_constant = true; + base.type.class_type = p_context.current_class; + base.value = p_context.base; + } else { + break; + } + } else if (callee_type == GDScriptParser::Node::SUBSCRIPT && static_cast<const GDScriptParser::SubscriptNode *>(call->callee)->is_attribute) { + if (!_guess_expression_type(c, static_cast<const GDScriptParser::SubscriptNode *>(call->callee)->base, base)) { found = false; break; } + } else { + break; + } - // Try call if constant methods with constant arguments - if (base.type.is_constant && base.value.get_type() == Variant::OBJECT) { - GDScriptParser::DataType native_type = base.type; + // Try call if constant methods with constant arguments + if (base.type.is_constant && base.value.get_type() == Variant::OBJECT) { + GDScriptParser::DataType native_type = base.type; - while (native_type.kind == GDScriptParser::DataType::CLASS) { - native_type = native_type.class_type->base_type; - } + while (native_type.kind == GDScriptParser::DataType::CLASS) { + native_type = native_type.class_type->base_type; + } - while (native_type.kind == GDScriptParser::DataType::GDSCRIPT || native_type.kind == GDScriptParser::DataType::SCRIPT) { - if (native_type.script_type.is_valid()) { - Ref<Script> parent = native_type.script_type->get_base_script(); - if (parent.is_valid()) { - native_type.script_type = parent; - } else { - native_type.kind = GDScriptParser::DataType::NATIVE; - native_type.native_type = native_type.script_type->get_instance_base_type(); + while (native_type.kind == GDScriptParser::DataType::SCRIPT) { + if (native_type.script_type.is_valid()) { + Ref<Script> parent = native_type.script_type->get_base_script(); + if (parent.is_valid()) { + native_type.script_type = parent; + } else { + native_type.kind = GDScriptParser::DataType::NATIVE; + native_type.native_type = native_type.script_type->get_instance_base_type(); + if (!ClassDB::class_exists(native_type.native_type)) { + native_type.native_type = String("_") + native_type.native_type; if (!ClassDB::class_exists(native_type.native_type)) { - native_type.native_type = String("_") + native_type.native_type; - if (!ClassDB::class_exists(native_type.native_type)) { - native_type.has_type = false; - } + native_type.kind = GDScriptParser::DataType::UNRESOLVED; } } } } + } - if (native_type.has_type && native_type.kind == GDScriptParser::DataType::NATIVE) { - MethodBind *mb = ClassDB::get_method(native_type.native_type, id); - if (mb && mb->is_const()) { - bool all_is_const = true; - Vector<Variant> args; - GDScriptCompletionContext c2 = p_context; - c2.line = op->line; - for (int i = 2; all_is_const && i < op->arguments.size(); i++) { - GDScriptCompletionIdentifier arg; - - if (_guess_expression_type(c2, op->arguments[i], arg)) { - if (arg.type.has_type && arg.type.is_constant && arg.value.get_type() != Variant::OBJECT) { - args.push_back(arg.value); - } else { - all_is_const = false; - } - } else { - all_is_const = false; - } + if (native_type.kind == GDScriptParser::DataType::NATIVE) { + MethodBind *mb = ClassDB::get_method(native_type.native_type, call->function_name); + if (mb && mb->is_const()) { + bool all_is_const = true; + Vector<Variant> args; + GDScriptParser::CompletionContext c2 = p_context; + c2.current_line = call->start_line; + for (int i = 0; all_is_const && i < call->arguments.size(); i++) { + GDScriptCompletionIdentifier arg; + + if (!call->arguments[i]->is_constant) { + all_is_const = false; } + } - Object *baseptr = base.value; - - if (all_is_const && String(id) == "get_node" && ClassDB::is_parent_class(native_type.native_type, "Node") && args.size()) { - String arg1 = args[0]; - if (arg1.begins_with("/root/")) { - String which = arg1.get_slice("/", 2); - if (which != "") { - // Try singletons first - if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(which)) { - r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which]); - found = true; - } else { - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - String name = s.get_slice("/", 1); - if (name == which) { - String script = ProjectSettings::get_singleton()->get(s); + Object *baseptr = base.value; - if (script.begins_with("*")) { - script = script.right(1); - } + if (all_is_const && String(call->function_name) == "get_node" && ClassDB::is_parent_class(native_type.native_type, "Node") && args.size()) { + String arg1 = args[0]; + if (arg1.begins_with("/root/")) { + String which = arg1.get_slice("/", 2); + if (which != "") { + // Try singletons first + if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(which)) { + r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which]); + found = true; + } else { + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); - if (!script.begins_with("res://")) { - script = "res://" + script; - } + for (Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E; E = E->next()) { + String name = E->key(); + if (name == which) { + String script = E->value().path; - if (!script.ends_with(".gd")) { - //not a script, try find the script anyway, - //may have some success - script = script.get_basename() + ".gd"; - } + if (!script.begins_with("res://")) { + script = "res://" + script; + } + + if (!script.ends_with(".gd")) { + //not a script, try find the script anyway, + //may have some success + script = script.get_basename() + ".gd"; + } - if (FileAccess::exists(script)) { - Ref<Script> scr; - if (ScriptCodeCompletionCache::get_singleton()) { - scr = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(script); - } else { - scr = ResourceLoader::load(script); - } - if (scr.is_valid()) { - r_type.type.has_type = true; - r_type.type.script_type = scr; - r_type.type.is_constant = false; - Ref<GDScript> gds = scr; - if (gds.is_valid()) { - r_type.type.kind = GDScriptParser::DataType::GDSCRIPT; - } else { - r_type.type.kind = GDScriptParser::DataType::SCRIPT; - } - r_type.value = Variant(); - found = true; - } + if (FileAccess::exists(script)) { + Error err = OK; + Ref<GDScriptParserRef> parser = GDScriptCache::get_parser(script, GDScriptParserRef::INTERFACE_SOLVED, err); + if (err == OK) { + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.script_path = script; + r_type.type.class_type = parser->get_parser()->get_tree(); + r_type.type.is_constant = false; + r_type.type.kind = GDScriptParser::DataType::CLASS; + r_type.value = Variant(); + p_context.dependent_parsers.push_back(parser); + found = true; } - break; } + break; } } } } } + } - if (!found && all_is_const && baseptr) { - Vector<const Variant *> argptr; - for (int i = 0; i < args.size(); i++) { - argptr.push_back(&args[i]); - } + if (!found && all_is_const && baseptr) { + Vector<const Variant *> argptr; + for (int i = 0; i < args.size(); i++) { + argptr.push_back(&args[i]); + } - Callable::CallError ce; - Variant ret = mb->call(baseptr, (const Variant **)argptr.ptr(), argptr.size(), ce); + Callable::CallError ce; + Variant ret = mb->call(baseptr, (const Variant **)argptr.ptr(), argptr.size(), ce); - if (ce.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) { - if (ret.get_type() != Variant::OBJECT || ret.operator Object *() != nullptr) { - r_type = _type_from_variant(ret); - found = true; - } + if (ce.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) { + if (ret.get_type() != Variant::OBJECT || ret.operator Object *() != nullptr) { + r_type = _type_from_variant(ret); + found = true; } } } } } - - if (!found) { - found = _guess_method_return_type_from_base(c, base, id, r_type); - } - } - } break; - case GDScriptParser::OperatorNode::OP_PARENT_CALL: { - if (!p_context._class || !op->arguments.size() || op->arguments[0]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - break; } - StringName id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[0])->name; - - GDScriptCompletionIdentifier base; - base.value = p_context.base; - base.type = p_context._class->base_type; - - GDScriptCompletionContext c = p_context; - c.line = op->line; - - found = _guess_method_return_type_from_base(c, base, id, r_type); - } break; - case GDScriptParser::OperatorNode::OP_INDEX_NAMED: { - if (op->arguments.size() < 2 || op->arguments[1]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - found = false; - break; + if (!found) { + found = _guess_method_return_type_from_base(c, base, call->function_name, r_type); } - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[1]); - - GDScriptCompletionContext c = p_context; - c.line = op->line; + } + } break; + case GDScriptParser::Node::SUBSCRIPT: { + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(p_expression); + if (subscript->is_attribute) { + GDScriptParser::CompletionContext c = p_context; + c.current_line = subscript->start_line; GDScriptCompletionIdentifier base; - if (!_guess_expression_type(c, op->arguments[0], base)) { + if (!_guess_expression_type(c, subscript->base, base)) { found = false; break; } - if (base.value.get_type() == Variant::DICTIONARY && base.value.operator Dictionary().has(String(id->name))) { - Variant value = base.value.operator Dictionary()[String(id->name)]; + if (base.value.get_type() == Variant::DICTIONARY && base.value.operator Dictionary().has(String(subscript->attribute->name))) { + Variant value = base.value.operator Dictionary()[String(subscript->attribute->name)]; r_type = _type_from_variant(value); found = true; break; } const GDScriptParser::DictionaryNode *dn = nullptr; - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_DICTIONARY) { - dn = static_cast<const GDScriptParser::DictionaryNode *>(op->arguments[0]); - } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::TYPE_DICTIONARY) { + if (subscript->base->type == GDScriptParser::Node::DICTIONARY) { + dn = static_cast<const GDScriptParser::DictionaryNode *>(subscript->base); + } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::DICTIONARY) { dn = static_cast<const GDScriptParser::DictionaryNode *>(base.assigned_expression); } @@ -945,7 +1431,7 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G if (!_guess_expression_type(c, dn->elements[i].key, key)) { continue; } - if (key.value == String(id->name)) { + if (key.value == String(subscript->attribute->name)) { r_type.assigned_expression = dn->elements[i].value; found = _guess_expression_type(c, dn->elements[i].value, r_type); break; @@ -954,26 +1440,25 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G } if (!found) { - found = _guess_identifier_type_from_base(c, base, id->name, r_type); + found = _guess_identifier_type_from_base(c, base, subscript->attribute->name, r_type); } - } break; - case GDScriptParser::OperatorNode::OP_INDEX: { - if (op->arguments.size() < 2) { + } else { + if (subscript->index == nullptr) { found = false; break; } - GDScriptCompletionContext c = p_context; - c.line = op->line; + GDScriptParser::CompletionContext c = p_context; + c.current_line = subscript->start_line; GDScriptCompletionIdentifier base; - if (!_guess_expression_type(c, op->arguments[0], base)) { + if (!_guess_expression_type(c, subscript->base, base)) { found = false; break; } GDScriptCompletionIdentifier index; - if (!_guess_expression_type(c, op->arguments[1], index)) { + if (!_guess_expression_type(c, subscript->index, index)) { found = false; break; } @@ -985,11 +1470,11 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G break; } - // Look if it is a dictionary node + // Look if it is a dictionary node. const GDScriptParser::DictionaryNode *dn = nullptr; - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_DICTIONARY) { - dn = static_cast<const GDScriptParser::DictionaryNode *>(op->arguments[0]); - } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::TYPE_DICTIONARY) { + if (subscript->base->type == GDScriptParser::Node::DICTIONARY) { + dn = static_cast<const GDScriptParser::DictionaryNode *>(subscript->base); + } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::DICTIONARY) { dn = static_cast<const GDScriptParser::DictionaryNode *>(base.assigned_expression); } @@ -1007,13 +1492,13 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G } } - // Look if it is an array node + // Look if it is an array node. if (!found && index.value.is_num()) { int idx = index.value; const GDScriptParser::ArrayNode *an = nullptr; - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_ARRAY) { - an = static_cast<const GDScriptParser::ArrayNode *>(op->arguments[0]); - } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::TYPE_ARRAY) { + if (subscript->base->type == GDScriptParser::Node::ARRAY) { + an = static_cast<const GDScriptParser::ArrayNode *>(subscript->base); + } else if (base.assigned_expression && base.assigned_expression->type == GDScriptParser::Node::ARRAY) { an = static_cast<const GDScriptParser::ArrayNode *>(base.assigned_expression); } @@ -1040,113 +1525,74 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G found = true; } } - } break; - default: { - if (op->arguments.size() < 2) { - found = false; - break; - } - - Variant::Operator vop = Variant::OP_MAX; - switch (op->op) { - case GDScriptParser::OperatorNode::OP_ADD: - vop = Variant::OP_ADD; - break; - case GDScriptParser::OperatorNode::OP_SUB: - vop = Variant::OP_SUBTRACT; - break; - case GDScriptParser::OperatorNode::OP_MUL: - vop = Variant::OP_MULTIPLY; - break; - case GDScriptParser::OperatorNode::OP_DIV: - vop = Variant::OP_DIVIDE; - break; - case GDScriptParser::OperatorNode::OP_MOD: - vop = Variant::OP_MODULE; - break; - case GDScriptParser::OperatorNode::OP_SHIFT_LEFT: - vop = Variant::OP_SHIFT_LEFT; - break; - case GDScriptParser::OperatorNode::OP_SHIFT_RIGHT: - vop = Variant::OP_SHIFT_RIGHT; - break; - case GDScriptParser::OperatorNode::OP_BIT_AND: - vop = Variant::OP_BIT_AND; - break; - case GDScriptParser::OperatorNode::OP_BIT_OR: - vop = Variant::OP_BIT_OR; - break; - case GDScriptParser::OperatorNode::OP_BIT_XOR: - vop = Variant::OP_BIT_XOR; - break; - default: { - } - } + } + } break; + case GDScriptParser::Node::BINARY_OPERATOR: { + const GDScriptParser::BinaryOpNode *op = static_cast<const GDScriptParser::BinaryOpNode *>(p_expression); - if (vop == Variant::OP_MAX) { - break; - } + if (op->variant_op == Variant::OP_MAX) { + break; + } - GDScriptCompletionContext context = p_context; - context.line = op->line; + GDScriptParser::CompletionContext context = p_context; + context.current_line = op->start_line; - GDScriptCompletionIdentifier p1; - GDScriptCompletionIdentifier p2; + GDScriptCompletionIdentifier p1; + GDScriptCompletionIdentifier p2; - if (!_guess_expression_type(context, op->arguments[0], p1)) { - found = false; - break; - } + if (!_guess_expression_type(context, op->left_operand, p1)) { + found = false; + break; + } - if (!_guess_expression_type(context, op->arguments[1], p2)) { - found = false; - break; - } + if (!_guess_expression_type(context, op->right_operand, p2)) { + found = false; + break; + } - Callable::CallError ce; - bool v1_use_value = p1.value.get_type() != Variant::NIL && p1.value.get_type() != Variant::OBJECT; - Variant v1 = (v1_use_value) ? p1.value : Variant::construct(p1.type.builtin_type, nullptr, 0, ce); - bool v2_use_value = p2.value.get_type() != Variant::NIL && p2.value.get_type() != Variant::OBJECT; - Variant v2 = (v2_use_value) ? p2.value : Variant::construct(p2.type.builtin_type, nullptr, 0, ce); - // avoid potential invalid ops - if ((vop == Variant::OP_DIVIDE || vop == Variant::OP_MODULE) && v2.get_type() == Variant::INT) { - v2 = 1; - v2_use_value = false; - } - if (vop == Variant::OP_DIVIDE && v2.get_type() == Variant::FLOAT) { - v2 = 1.0; - v2_use_value = false; - } + Callable::CallError ce; + bool v1_use_value = p1.value.get_type() != Variant::NIL && p1.value.get_type() != Variant::OBJECT; + Variant v1 = (v1_use_value) ? p1.value : Variant::construct(p1.type.builtin_type, nullptr, 0, ce); + bool v2_use_value = p2.value.get_type() != Variant::NIL && p2.value.get_type() != Variant::OBJECT; + Variant v2 = (v2_use_value) ? p2.value : Variant::construct(p2.type.builtin_type, nullptr, 0, ce); + // avoid potential invalid ops + if ((op->variant_op == Variant::OP_DIVIDE || op->variant_op == Variant::OP_MODULE) && v2.get_type() == Variant::INT) { + v2 = 1; + v2_use_value = false; + } + if (op->variant_op == Variant::OP_DIVIDE && v2.get_type() == Variant::FLOAT) { + v2 = 1.0; + v2_use_value = false; + } - Variant res; - bool valid; - Variant::evaluate(vop, v1, v2, res, valid); - if (!valid) { - found = false; - break; - } - r_type = _type_from_variant(res); - if (!v1_use_value || !v2_use_value) { - r_type.value = Variant(); - r_type.type.is_constant = false; - } + Variant res; + bool valid; + Variant::evaluate(op->variant_op, v1, v2, res, valid); + if (!valid) { + found = false; + break; + } + r_type = _type_from_variant(res); + if (!v1_use_value || !v2_use_value) { + r_type.value = Variant(); + r_type.type.is_constant = false; + } - found = true; - } break; - } - } break; - default: { + found = true; + } break; + default: + break; } } // It may have found a null, but that's never useful - if (found && r_type.type.has_type && r_type.type.kind == GDScriptParser::DataType::BUILTIN && r_type.type.builtin_type == Variant::NIL) { + if (found && r_type.type.kind == GDScriptParser::DataType::BUILTIN && r_type.type.builtin_type == Variant::NIL) { found = false; } // Check type hint last. For collections we want chance to get the actual value first // This way we can detect types from the content of dictionaries and arrays - if (!found && p_expression->get_datatype().has_type) { + if (!found && p_expression->get_datatype().is_hard_type()) { r_type.type = p_expression->get_datatype(); if (!r_type.assigned_expression) { r_type.assigned_expression = p_expression; @@ -1157,306 +1603,289 @@ static bool _guess_expression_type(GDScriptCompletionContext &p_context, const G return found; } -static bool _guess_identifier_type(GDScriptCompletionContext &p_context, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type) { - // Look in blocks first - const GDScriptParser::BlockNode *blk = p_context.block; +static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type) { + // Look in blocks first. int last_assign_line = -1; - const GDScriptParser::Node *last_assigned_expression = nullptr; - GDScriptParser::DataType var_type; - while (blk) { - if (blk->variables.has(p_identifier)) { - if (blk->variables[p_identifier]->line > p_context.line) { - return false; - } + const GDScriptParser::ExpressionNode *last_assigned_expression = nullptr; + GDScriptParser::DataType id_type; + GDScriptParser::SuiteNode *suite = p_context.current_suite; + bool is_function_parameter = false; - var_type = blk->variables[p_identifier]->datatype; + if (suite) { + if (suite->has_local(p_identifier)) { + const GDScriptParser::SuiteNode::Local &local = suite->get_local(p_identifier); - if (!last_assigned_expression && blk->variables[p_identifier]->assign && blk->variables[p_identifier]->assign->type == GDScriptParser::Node::TYPE_OPERATOR) { - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(blk->variables[p_identifier]->assign); - if (op->op == GDScriptParser::OperatorNode::OP_ASSIGN && op->arguments.size() >= 2) { - last_assign_line = op->line; - last_assigned_expression = op->arguments[1]; - } - } - } + id_type = local.get_datatype(); - for (const List<GDScriptParser::Node *>::Element *E = blk->statements.front(); E; E = E->next()) { - const GDScriptParser::Node *expr = E->get(); - if (expr->line > p_context.line || expr->type != GDScriptParser::Node::TYPE_OPERATOR) { - continue; + // Check initializer as the first assignment. + switch (local.type) { + case GDScriptParser::SuiteNode::Local::VARIABLE: + if (local.variable->initializer) { + last_assign_line = local.variable->initializer->end_line; + last_assigned_expression = local.variable->initializer; + } + break; + case GDScriptParser::SuiteNode::Local::CONSTANT: + if (local.constant->initializer) { + last_assign_line = local.constant->initializer->end_line; + last_assigned_expression = local.constant->initializer; + } + break; + case GDScriptParser::SuiteNode::Local::PARAMETER: + if (local.parameter->default_value) { + last_assign_line = local.parameter->default_value->end_line; + last_assigned_expression = local.parameter->default_value; + } + is_function_parameter = true; + break; + default: + break; } + } + } - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(expr); - if (op->op != GDScriptParser::OperatorNode::OP_ASSIGN || op->line < last_assign_line) { - continue; + while (suite) { + for (int i = 0; i < suite->statements.size(); i++) { + if (suite->statements[i]->start_line > p_context.current_line) { + break; } - if (op->arguments.size() >= 2 && op->arguments[0]->type == GDScriptParser::Node::TYPE_IDENTIFIER) { - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[0]); - if (id->name == p_identifier) { - last_assign_line = op->line; - last_assigned_expression = op->arguments[1]; - } + switch (suite->statements[i]->type) { + case GDScriptParser::Node::ASSIGNMENT: { + const GDScriptParser::AssignmentNode *assign = static_cast<const GDScriptParser::AssignmentNode *>(suite->statements[i]); + if (assign->end_line > last_assign_line && assign->assignee && assign->assigned_value && assign->assignee->type == GDScriptParser::Node::IDENTIFIER) { + const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(assign->assignee); + if (id->name == p_identifier) { + last_assign_line = assign->assigned_value->end_line; + last_assigned_expression = assign->assigned_value; + } + } + } break; + default: + // TODO: Check sub blocks (control flow statements) as they might also reassign stuff. + break; } } - if (blk->if_condition && blk->if_condition->type == GDScriptParser::Node::TYPE_OPERATOR && static_cast<const GDScriptParser::OperatorNode *>(blk->if_condition)->op == GDScriptParser::OperatorNode::OP_IS) { - //is used, check if identifier is in there! this helps resolve in blocks that are (if (identifier is value)): which are very common.. - //super dirty hack, but very useful - //credit: Zylann - //TODO: this could be hacked to detect ANDed conditions too.. - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(blk->if_condition); - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_IDENTIFIER && static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[0])->name == p_identifier) { - //bingo - GDScriptCompletionContext c = p_context; - c.line = op->line; - c.block = blk; - if (_guess_expression_type(p_context, op->arguments[1], r_type)) { - r_type.type.is_meta_type = false; // Right-hand of `is` will be a meta type, but the left-hand value is not - // Not an assignment, it shouldn't carry any value - r_type.value = Variant(); - r_type.assigned_expression = nullptr; - - return true; + if (suite->parent_if && suite->parent_if->condition && suite->parent_if->condition->type == GDScriptParser::Node::BINARY_OPERATOR && static_cast<const GDScriptParser::BinaryOpNode *>(suite->parent_if->condition)->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST) { + // Operator `is` used, check if identifier is in there! this helps resolve in blocks that are (if (identifier is value)): which are very common.. + // Super dirty hack, but very useful. + // Credit: Zylann. + // TODO: this could be hacked to detect ANDed conditions too... + const GDScriptParser::BinaryOpNode *op = static_cast<const GDScriptParser::BinaryOpNode *>(suite->parent_if->condition); + if (op->left_operand && op->right_operand && op->left_operand->type == GDScriptParser::Node::IDENTIFIER && static_cast<const GDScriptParser::IdentifierNode *>(op->left_operand)->name == p_identifier) { + // Bingo. + GDScriptParser::CompletionContext c = p_context; + c.current_line = op->left_operand->start_line; + c.current_suite = suite; + GDScriptCompletionIdentifier is_type; + if (_guess_expression_type(c, op->right_operand, is_type)) { + id_type = is_type.type; + id_type.is_meta_type = false; + if (last_assign_line < c.current_line) { + // Override last assignment. + last_assign_line = c.current_line; + last_assigned_expression = nullptr; + } } } } - blk = blk->parent_block; + suite = suite->parent_block; } - if (last_assigned_expression && last_assign_line != p_context.line) { - GDScriptCompletionContext c = p_context; - c.line = last_assign_line; + if (last_assigned_expression && last_assign_line != p_context.current_line) { + GDScriptParser::CompletionContext c = p_context; + c.current_line = last_assign_line; r_type.assigned_expression = last_assigned_expression; if (_guess_expression_type(c, last_assigned_expression, r_type)) { - if (var_type.has_type) { - r_type.type = var_type; - } return true; } } - if (var_type.has_type) { - r_type.type = var_type; - return true; - } - - if (p_context.function) { - for (int i = 0; i < p_context.function->arguments.size(); i++) { - if (p_context.function->arguments[i] == p_identifier) { - if (p_context.function->argument_types[i].has_type) { - r_type.type = p_context.function->argument_types[i]; - return true; - } - - int def_from = p_context.function->arguments.size() - p_context.function->default_values.size(); - if (i >= def_from) { - int def_idx = i - def_from; - if (p_context.function->default_values[def_idx]->type == GDScriptParser::Node::TYPE_OPERATOR) { - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(p_context.function->default_values[def_idx]); - if (op->arguments.size() < 2) { - return false; - } - GDScriptCompletionContext c = p_context; - c.function = nullptr; - c.block = nullptr; - return _guess_expression_type(c, op->arguments[1], r_type); - } - } - break; - } - } - - GDScriptParser::DataType base_type = p_context._class->base_type; - while (base_type.has_type) { + if (is_function_parameter && p_context.current_function && p_context.current_class) { + // Check if it's override of native function, then we can assume the type from the signature. + GDScriptParser::DataType base_type = p_context.current_class->base_type; + while (base_type.is_set()) { switch (base_type.kind) { - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> gds = base_type.script_type; - if (gds.is_valid() && gds->has_method(p_context.function->name)) { - GDScriptFunction *func = gds->get_member_functions()[p_context.function->name]; - if (func) { - for (int i = 0; i < func->get_argument_count(); i++) { - if (func->get_argument_name(i) == p_identifier) { - r_type = _type_from_gdtype(func->get_argument_type(i)); - return true; - } - } + case GDScriptParser::DataType::CLASS: + if (base_type.class_type->has_function(p_context.current_function->identifier->name)) { + GDScriptParser::FunctionNode *parent_function = base_type.class_type->get_member(p_context.current_function->identifier->name).function; + const GDScriptParser::ParameterNode *parameter = parent_function->parameters[parent_function->parameters_indices[p_identifier]]; + if ((!id_type.is_set() || id_type.is_variant()) && parameter->get_datatype().is_hard_type()) { + id_type = parameter->get_datatype(); } - Ref<GDScript> base_gds = gds->get_base_script(); - if (base_gds.is_valid()) { - base_type.kind = GDScriptParser::DataType::GDSCRIPT; - base_type.script_type = base_gds; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = gds->get_instance_base_type(); + if (parameter->default_value) { + GDScriptParser::CompletionContext c = p_context; + c.current_function = parent_function; + c.current_class = base_type.class_type; + c.base = nullptr; + if (_guess_expression_type(c, parameter->default_value, r_type)) { + return true; + } } - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = gds->get_instance_base_type(); + base_type = base_type.class_type->base_type; } - } break; + break; case GDScriptParser::DataType::NATIVE: { - List<MethodInfo> methods; - ClassDB::get_method_list(base_type.native_type, &methods); - ClassDB::get_virtual_methods(base_type.native_type, &methods); - - for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().name == p_context.function->name) { - MethodInfo &mi = E->get(); - for (List<PropertyInfo>::Element *F = mi.arguments.front(); F; F = F->next()) { - if (F->get().name == p_identifier) { - r_type = _type_from_property(F->get()); - return true; - } + if (id_type.is_set() && !id_type.is_variant()) { + base_type = GDScriptParser::DataType(); + break; + } + StringName real_native = _get_real_class_name(base_type.native_type); + MethodInfo info; + if (ClassDB::get_method_info(real_native, p_context.current_function->identifier->name, &info)) { + for (const List<PropertyInfo>::Element *E = info.arguments.front(); E; E = E->next()) { + if (E->get().name == p_identifier) { + r_type = _type_from_property(E->get()); + return true; } } } - base_type.has_type = false; - } break; - default: { - base_type.has_type = false; + base_type = GDScriptParser::DataType(); } break; + default: + break; } } } - // Check current class (including inheritance) - if (p_context._class) { - GDScriptCompletionIdentifier context_base; - context_base.value = p_context.base; - context_base.type.has_type = true; - context_base.type.kind = GDScriptParser::DataType::CLASS; - context_base.type.class_type = const_cast<GDScriptParser::ClassNode *>(p_context._class); - context_base.type.is_meta_type = p_context.function && p_context.function->_static; - - if (_guess_identifier_type_from_base(p_context, context_base, p_identifier, r_type)) { - return true; - } + if (id_type.is_set() && !id_type.is_variant()) { + r_type.type = id_type; + return true; } - // Check named scripts - if (ScriptServer::is_global_class(p_identifier)) { - Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier)); - if (scr.is_valid()) { - r_type = _type_from_variant(scr); - r_type.type.is_meta_type = true; + // Check current class (including inheritance). + if (p_context.current_class) { + GDScriptCompletionIdentifier base; + base.value = p_context.base; + base.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + base.type.kind = GDScriptParser::DataType::CLASS; + base.type.class_type = p_context.current_class; + base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static; + + if (_guess_identifier_type_from_base(p_context, base, p_identifier, r_type)) { return true; } - return false; } - for (int i = 0; i < 2; i++) { - StringName target_id; - switch (i) { - case 0: - // Check ClassDB - target_id = p_identifier; - break; - case 1: - // ClassDB again for underscore-prefixed classes - target_id = String("_") + p_identifier; - break; - } - - if (ClassDB::class_exists(target_id)) { - r_type.type.has_type = true; - r_type.type.kind = GDScriptParser::DataType::NATIVE; - r_type.type.native_type = target_id; - if (Engine::get_singleton()->has_singleton(target_id)) { - r_type.type.is_meta_type = false; - r_type.value = Engine::get_singleton()->get_singleton_object(target_id); - } else { + // Check global scripts. + if (ScriptServer::is_global_class(p_identifier)) { + String script = ScriptServer::get_global_class_path(p_identifier); + if (script.to_lower().ends_with(".gd")) { + Error err = OK; + Ref<GDScriptParserRef> parser = GDScriptCache::get_parser(script, GDScriptParserRef::INTERFACE_SOLVED, err); + if (err == OK) { + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.script_path = script; + r_type.type.class_type = parser->get_parser()->get_tree(); + r_type.type.is_constant = false; + r_type.type.kind = GDScriptParser::DataType::CLASS; + r_type.value = Variant(); + p_context.dependent_parsers.push_back(parser); + return true; + } + } else { + Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier)); + if (scr.is_valid()) { + r_type = _type_from_variant(scr); r_type.type.is_meta_type = true; - const Map<StringName, int>::Element *target_elem = GDScriptLanguage::get_singleton()->get_global_map().find(target_id); - // Check because classes like EditorNode are in ClassDB by now, but unknown to GDScript - if (!target_elem) { - return false; - } - int idx = target_elem->get(); - r_type.value = GDScriptLanguage::get_singleton()->get_global_array()[idx]; + return true; } - return true; } + return false; } - // Check autoload singletons - if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(p_identifier)) { + // Check autoloads. + if (ProjectSettings::get_singleton()->has_autoload(p_identifier)) { r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier]); return true; } + // Check ClassDB. + StringName class_name = _get_real_class_name(p_identifier); + if (ClassDB::class_exists(class_name) && ClassDB::is_class_exposed(class_name)) { + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::NATIVE; + r_type.type.native_type = p_identifier; + r_type.type.is_constant = true; + r_type.type.is_meta_type = !Engine::get_singleton()->has_singleton(class_name); + r_type.value = Variant(); + } + return false; } -static bool _guess_identifier_type_from_base(GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type) { +static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_identifier, GDScriptCompletionIdentifier &r_type) { GDScriptParser::DataType base_type = p_base.type; - bool _static = base_type.is_meta_type; - while (base_type.has_type) { + bool is_static = base_type.is_meta_type; + while (base_type.is_set()) { switch (base_type.kind) { - case GDScriptParser::DataType::CLASS: { - if (base_type.class_type->constant_expressions.has(p_identifier)) { - GDScriptParser::ClassNode::Constant c = base_type.class_type->constant_expressions[p_identifier]; - r_type.type = c.type; - if (c.expression->type == GDScriptParser::Node::TYPE_CONSTANT) { - r_type.value = static_cast<const GDScriptParser::ConstantNode *>(c.expression)->value; - } - return true; - } - - if (!_static) { - for (int i = 0; i < base_type.class_type->variables.size(); i++) { - GDScriptParser::ClassNode::Member m = base_type.class_type->variables[i]; - if (m.identifier == p_identifier) { - if (m.expression) { - if (p_context.line == m.expression->line) { - // Variable used in the same expression - return false; - } - - if (_guess_expression_type(p_context, m.expression, r_type)) { - return true; - } - if (m.expression->get_datatype().has_type) { - r_type.type = m.expression->get_datatype(); + case GDScriptParser::DataType::CLASS: + if (base_type.class_type->has_member(p_identifier)) { + const GDScriptParser::ClassNode::Member &member = base_type.class_type->get_member(p_identifier); + switch (member.type) { + case GDScriptParser::ClassNode::Member::CONSTANT: + r_type.type = member.constant->get_datatype(); + if (member.constant->initializer && member.constant->initializer->is_constant) { + r_type.value = member.constant->initializer->reduced_value; + } + return true; + case GDScriptParser::ClassNode::Member::VARIABLE: + if (!is_static) { + if (member.variable->initializer) { + const GDScriptParser::ExpressionNode *init = member.variable->initializer; + if (init->is_constant) { + r_type.value = init->reduced_value; + r_type = _type_from_variant(init->reduced_value); + return true; + } else if (init->start_line == p_context.current_line) { + return false; + } else if (_guess_expression_type(p_context, init, r_type)) { + return true; + } else if (init->get_datatype().is_set() && !init->get_datatype().is_variant()) { + r_type.type = init->get_datatype(); + return true; + } + } else if (member.variable->get_datatype().is_set() && !member.variable->get_datatype().is_variant()) { + r_type.type = member.variable->get_datatype(); return true; } } - if (m.data_type.has_type) { - r_type.type = m.data_type; - return true; - } + // TODO: Check assignments in constructor. return false; - } - } - } - base_type = base_type.class_type->base_type; - } break; - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> gds = base_type.script_type; - if (gds.is_valid()) { - if (gds->get_constants().has(p_identifier)) { - r_type = _type_from_variant(gds->get_constants()[p_identifier]); - return true; - } - if (!_static) { - const Set<StringName>::Element *m = gds->get_members().find(p_identifier); - if (m) { - r_type = _type_from_gdtype(gds->get_member_type(p_identifier)); + case GDScriptParser::ClassNode::Member::ENUM: + r_type.type = member.m_enum->get_datatype(); + r_type.enumeration = member.m_enum->identifier->name; return true; - } - } - Ref<GDScript> parent = gds->get_base_script(); - if (parent.is_valid()) { - base_type.script_type = parent; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = gds->get_instance_base_type(); + case GDScriptParser::ClassNode::Member::ENUM_VALUE: + r_type = _type_from_variant(member.enum_value.value); + return true; + case GDScriptParser::ClassNode::Member::SIGNAL: + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::BUILTIN; + r_type.type.builtin_type = Variant::SIGNAL; + return true; + case GDScriptParser::ClassNode::Member::FUNCTION: + if (is_static && !member.function->is_static) { + return false; + } + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::BUILTIN; + r_type.type.builtin_type = Variant::CALLABLE; + return true; + case GDScriptParser::ClassNode::Member::CLASS: + r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + r_type.type.kind = GDScriptParser::DataType::CLASS; + r_type.type.class_type = member.m_class; + return true; + case GDScriptParser::ClassNode::Member::UNDEFINED: + return false; // Unreachable. } - } else { return false; } - } break; + base_type = base_type.class_type->base_type; + break; case GDScriptParser::DataType::SCRIPT: { Ref<Script> scr = base_type.script_type; if (scr.is_valid()) { @@ -1467,7 +1896,7 @@ static bool _guess_identifier_type_from_base(GDScriptCompletionContext &p_contex return true; } - if (!_static) { + if (!is_static) { List<PropertyInfo> members; scr->get_script_property_list(&members); for (const List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { @@ -1490,33 +1919,25 @@ static bool _guess_identifier_type_from_base(GDScriptCompletionContext &p_contex } } break; case GDScriptParser::DataType::NATIVE: { - StringName class_name = base_type.native_type; + StringName class_name = _get_real_class_name(base_type.native_type); if (!ClassDB::class_exists(class_name)) { - class_name = String("_") + class_name; - if (!ClassDB::class_exists(class_name)) { - return false; - } + return false; } - // Skip constants since they're all integers. Type does not matter because int has no members + // Skip constants since they're all integers. Type does not matter because int has no members. - List<PropertyInfo> props; - ClassDB::get_property_list(class_name, &props); - for (const List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - const PropertyInfo &prop = E->get(); - if (prop.name == p_identifier) { - StringName getter = ClassDB::get_property_getter(class_name, p_identifier); - if (getter != StringName()) { - MethodBind *g = ClassDB::get_method(class_name, getter); - if (g) { - r_type = _type_from_property(g->get_return_info()); - return true; - } - } else { - r_type = _type_from_property(prop); + PropertyInfo prop; + if (ClassDB::get_property_info(class_name, p_identifier, &prop)) { + StringName getter = ClassDB::get_property_getter(class_name, p_identifier); + if (getter != StringName()) { + MethodBind *g = ClassDB::get_method(class_name, getter); + if (g) { + r_type = _type_from_property(g->get_return_info()); return true; } - break; + } else { + r_type = _type_from_property(prop); + return true; } } return false; @@ -1543,116 +1964,99 @@ static bool _guess_identifier_type_from_base(GDScriptCompletionContext &p_contex } break; } } - return false; } -static bool _find_last_return_in_block(const GDScriptCompletionContext &p_context, int &r_last_return_line, const GDScriptParser::Node **r_last_returned_value) { - if (!p_context.block) { - return false; +static void _find_last_return_in_block(GDScriptParser::CompletionContext &p_context, int &r_last_return_line, const GDScriptParser::ExpressionNode **r_last_returned_value) { + if (!p_context.current_suite) { + return; } - for (int i = 0; i < p_context.block->statements.size(); i++) { - if (p_context.block->statements[i]->line < r_last_return_line) { - continue; - } - if (p_context.block->statements[i]->type != GDScriptParser::Node::TYPE_CONTROL_FLOW) { - continue; + for (int i = 0; i < p_context.current_suite->statements.size(); i++) { + if (p_context.current_suite->statements[i]->start_line < r_last_return_line) { + break; } - const GDScriptParser::ControlFlowNode *cf = static_cast<const GDScriptParser::ControlFlowNode *>(p_context.block->statements[i]); - if (cf->cf_type == GDScriptParser::ControlFlowNode::CF_RETURN && cf->arguments.size() > 0) { - if (cf->line > r_last_return_line) { - r_last_return_line = cf->line; - *r_last_returned_value = cf->arguments[0]; - } + GDScriptParser::CompletionContext c = p_context; + switch (p_context.current_suite->statements[i]->type) { + case GDScriptParser::Node::FOR: + c.current_suite = static_cast<const GDScriptParser::ForNode *>(p_context.current_suite->statements[i])->loop; + _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); + break; + case GDScriptParser::Node::WHILE: + c.current_suite = static_cast<const GDScriptParser::WhileNode *>(p_context.current_suite->statements[i])->loop; + _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); + break; + case GDScriptParser::Node::IF: { + const GDScriptParser::IfNode *_if = static_cast<const GDScriptParser::IfNode *>(p_context.current_suite->statements[i]); + c.current_suite = _if->true_block; + _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); + if (_if->false_block) { + c.current_suite = _if->false_block; + _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); + } + } break; + case GDScriptParser::Node::MATCH: { + const GDScriptParser::MatchNode *match = static_cast<const GDScriptParser::MatchNode *>(p_context.current_suite->statements[i]); + for (int j = 0; j < match->branches.size(); j++) { + c.current_suite = match->branches[j]->block; + _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); + } + } break; + case GDScriptParser::Node::RETURN: { + const GDScriptParser::ReturnNode *ret = static_cast<const GDScriptParser::ReturnNode *>(p_context.current_suite->statements[i]); + if (ret->return_value) { + if (ret->start_line > r_last_return_line) { + r_last_return_line = ret->start_line; + *r_last_returned_value = ret->return_value; + } + } + } break; + default: + break; } } - - // Recurse into subblocks - for (int i = 0; i < p_context.block->sub_blocks.size(); i++) { - GDScriptCompletionContext c = p_context; - c.block = p_context.block->sub_blocks[i]; - _find_last_return_in_block(c, r_last_return_line, r_last_returned_value); - } - - return false; } -static bool _guess_method_return_type_from_base(GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, GDScriptCompletionIdentifier &r_type) { +static bool _guess_method_return_type_from_base(GDScriptParser::CompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, GDScriptCompletionIdentifier &r_type) { GDScriptParser::DataType base_type = p_base.type; - bool _static = base_type.is_meta_type; + bool is_static = base_type.is_meta_type; - if (_static && p_method == "new") { + if (is_static && p_method == "new") { r_type.type = base_type; r_type.type.is_meta_type = false; r_type.type.is_constant = false; return true; } - while (base_type.has_type) { + while (base_type.is_set() && !base_type.is_variant()) { switch (base_type.kind) { - case GDScriptParser::DataType::CLASS: { - if (!base_type.class_type) { - base_type.has_type = false; - break; - } - - for (int i = 0; i < base_type.class_type->static_functions.size(); i++) { - if (base_type.class_type->static_functions[i]->name == p_method) { + case GDScriptParser::DataType::CLASS: + if (base_type.class_type->has_function(p_method)) { + const GDScriptParser::FunctionNode *method = base_type.class_type->get_member(p_method).function; + if (!is_static || method->is_static) { int last_return_line = -1; - const GDScriptParser::Node *last_returned_value = nullptr; - GDScriptCompletionContext c = p_context; - c._class = base_type.class_type; - c.function = base_type.class_type->static_functions[i]; - c.block = c.function->body; + const GDScriptParser::ExpressionNode *last_returned_value = nullptr; + GDScriptParser::CompletionContext c = p_context; + c.current_class = base_type.class_type; + c.current_function = const_cast<GDScriptParser::FunctionNode *>(method); + c.current_suite = method->body; _find_last_return_in_block(c, last_return_line, &last_returned_value); if (last_returned_value) { - c.line = c.block->end_line; - return _guess_expression_type(c, last_returned_value, r_type); - } - } - } - if (!_static) { - for (int i = 0; i < base_type.class_type->functions.size(); i++) { - if (base_type.class_type->functions[i]->name == p_method) { - int last_return_line = -1; - const GDScriptParser::Node *last_returned_value = nullptr; - GDScriptCompletionContext c = p_context; - c._class = base_type.class_type; - c.function = base_type.class_type->functions[i]; - c.block = c.function->body; - - _find_last_return_in_block(c, last_return_line, &last_returned_value); - if (last_returned_value) { - c.line = c.block->end_line; - return _guess_expression_type(c, last_returned_value, r_type); + c.current_line = c.current_suite->end_line; + if (_guess_expression_type(c, last_returned_value, r_type)) { + return true; + } + if (method->get_datatype().is_set() && !method->get_datatype().is_variant()) { + r_type.type = method->get_datatype(); + return true; } } } } - base_type = base_type.class_type->base_type; - } break; - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> gds = base_type.script_type; - if (gds.is_valid()) { - if (gds->get_member_functions().has(p_method)) { - r_type = _type_from_gdtype(gds->get_member_functions()[p_method]->get_return_type()); - return true; - } - Ref<GDScript> base_script = gds->get_base_script(); - if (base_script.is_valid()) { - base_type.script_type = base_script; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = gds->get_instance_base_type(); - } - } else { - return false; - } - } break; + break; case GDScriptParser::DataType::SCRIPT: { Ref<Script> scr = base_type.script_type; if (scr.is_valid()) { @@ -1677,12 +2081,9 @@ static bool _guess_method_return_type_from_base(GDScriptCompletionContext &p_con } } break; case GDScriptParser::DataType::NATIVE: { - StringName native = base_type.native_type; + StringName native = _get_real_class_name(base_type.native_type); if (!ClassDB::class_exists(native)) { - native = String("_") + native; - if (!ClassDB::class_exists(native)) { - return false; - } + return false; } MethodBind *mb = ClassDB::get_method(native, p_method); if (mb) { @@ -1715,103 +2116,27 @@ static bool _guess_method_return_type_from_base(GDScriptCompletionContext &p_con } } } - return false; -} - -static String _make_arguments_hint(const MethodInfo &p_info, int p_arg_idx) { - String arghint = _get_visual_datatype(p_info.return_val, false) + " " + p_info.name + "("; - - int def_args = p_info.arguments.size() - p_info.default_arguments.size(); - int i = 0; - for (const List<PropertyInfo>::Element *E = p_info.arguments.front(); E; E = E->next()) { - if (i > 0) { - arghint += ", "; - } - - if (i == p_arg_idx) { - arghint += String::chr(0xFFFF); - } - arghint += E->get().name + ": " + _get_visual_datatype(E->get(), true); - - if (i - def_args >= 0) { - arghint += String(" = ") + p_info.default_arguments[i - def_args].get_construct_string(); - } - - if (i == p_arg_idx) { - arghint += String::chr(0xFFFF); - } - - i++; - } - - if (p_info.flags & METHOD_FLAG_VARARG) { - if (p_info.arguments.size() > 0) { - arghint += ", "; - } - if (p_arg_idx >= p_info.arguments.size()) { - arghint += String::chr(0xFFFF); - } - arghint += "..."; - if (p_arg_idx >= p_info.arguments.size()) { - arghint += String::chr(0xFFFF); - } - } - - arghint += ")"; - - return arghint; -} - -static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_function, int p_arg_idx) { - String arghint = p_function->return_type.to_string() + " " + p_function->name.operator String() + "("; - - int def_args = p_function->arguments.size() - p_function->default_values.size(); - for (int i = 0; i < p_function->arguments.size(); i++) { - if (i > 0) { - arghint += ", "; - } - - if (i == p_arg_idx) { - arghint += String::chr(0xFFFF); - } - arghint += p_function->arguments[i].operator String() + ": " + p_function->argument_types[i].to_string(); - - if (i - def_args >= 0) { - String def_val = "<unknown>"; - if (p_function->default_values[i - def_args] && p_function->default_values[i - def_args]->type == GDScriptParser::Node::TYPE_OPERATOR) { - const GDScriptParser::OperatorNode *assign = static_cast<const GDScriptParser::OperatorNode *>(p_function->default_values[i - def_args]); - - if (assign->arguments.size() >= 2) { - if (assign->arguments[1]->type == GDScriptParser::Node::TYPE_CONSTANT) { - const GDScriptParser::ConstantNode *cn = static_cast<const GDScriptParser::ConstantNode *>(assign->arguments[1]); - def_val = cn->value.get_construct_string(); - } else if (assign->arguments[1]->type == GDScriptParser::Node::TYPE_IDENTIFIER) { - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(assign->arguments[1]); - def_val = id->name.operator String(); - } - } - } - arghint += " = " + def_val; - } - if (i == p_arg_idx) { - arghint += String::chr(0xFFFF); - } - } - - arghint += ")"; - return arghint; + return false; } -static void _find_enumeration_candidates(const String p_enum_hint, Map<String, ScriptCodeCompletionOption> &r_result) { +static void _find_enumeration_candidates(GDScriptParser::CompletionContext &p_context, const String &p_enum_hint, Map<String, ScriptCodeCompletionOption> &r_result) { if (p_enum_hint.find(".") == -1) { - // Global constant + // Global constant or in the current class. StringName current_enum = p_enum_hint; - for (int i = 0; i < GlobalConstants::get_global_constant_count(); i++) { - if (GlobalConstants::get_global_constant_enum(i) == current_enum) { - ScriptCodeCompletionOption option(GlobalConstants::get_global_constant_name(i), ScriptCodeCompletionOption::KIND_ENUM); + if (p_context.current_class && p_context.current_class->has_member(current_enum) && p_context.current_class->get_member(current_enum).type == GDScriptParser::ClassNode::Member::ENUM) { + const GDScriptParser::EnumNode *_enum = p_context.current_class->get_member(current_enum).m_enum; + for (int i = 0; i < _enum->values.size(); i++) { + ScriptCodeCompletionOption option(_enum->values[i].identifier->name, ScriptCodeCompletionOption::KIND_ENUM); r_result.insert(option.display, option); } + } else { + for (int i = 0; i < GlobalConstants::get_global_constant_count(); i++) { + if (GlobalConstants::get_global_constant_enum(i) == current_enum) { + ScriptCodeCompletionOption option(GlobalConstants::get_global_constant_name(i), ScriptCodeCompletionOption::KIND_ENUM); + r_result.insert(option.display, option); + } + } } } else { String class_name = p_enum_hint.get_slice(".", 0); @@ -1831,500 +2156,60 @@ static void _find_enumeration_candidates(const String p_enum_hint, Map<String, S } } -static void _find_identifiers_in_block(const GDScriptCompletionContext &p_context, Map<String, ScriptCodeCompletionOption> &r_result) { - for (Map<StringName, GDScriptParser::LocalVarNode *>::Element *E = p_context.block->variables.front(); E; E = E->next()) { - if (E->get()->line < p_context.line) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_VARIABLE); - r_result.insert(option.display, option); - } - } - if (p_context.block->parent_block) { - GDScriptCompletionContext c = p_context; - c.block = p_context.block->parent_block; - _find_identifiers_in_block(c, r_result); - } -} - -static void _find_identifiers_in_base(const GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result); - -static void _find_identifiers_in_class(const GDScriptCompletionContext &p_context, bool p_static, bool p_only_functions, bool p_parent_only, Map<String, ScriptCodeCompletionOption> &r_result) { - if (!p_parent_only) { - if (!p_static && !p_only_functions) { - for (int i = 0; i < p_context._class->variables.size(); i++) { - ScriptCodeCompletionOption option(p_context._class->variables[i].identifier, ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - - if (!p_only_functions) { - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = p_context._class->constant_expressions.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->key(), ScriptCodeCompletionOption::KIND_CONSTANT); - r_result.insert(option.display, option); - } - for (int i = 0; i < p_context._class->subclasses.size(); i++) { - ScriptCodeCompletionOption option(p_context._class->subclasses[i]->name, ScriptCodeCompletionOption::KIND_CLASS); - r_result.insert(option.display, option); - } - } - - for (int i = 0; i < p_context._class->static_functions.size(); i++) { - ScriptCodeCompletionOption option(p_context._class->static_functions[i]->name.operator String(), ScriptCodeCompletionOption::KIND_FUNCTION); - if (p_context._class->static_functions[i]->arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - - if (!p_static) { - for (int i = 0; i < p_context._class->functions.size(); i++) { - ScriptCodeCompletionOption option(p_context._class->functions[i]->name.operator String(), ScriptCodeCompletionOption::KIND_FUNCTION); - if (p_context._class->functions[i]->arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - } - } - - // Parents - GDScriptCompletionIdentifier base_type; - base_type.type = p_context._class->base_type; - base_type.type.is_meta_type = p_static; - base_type.value = p_context.base; - - GDScriptCompletionContext c = p_context; - c.block = nullptr; - c.function = nullptr; - - _find_identifiers_in_base(c, base_type, p_only_functions, r_result); -} - -static void _find_identifiers_in_base(const GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { - GDScriptParser::DataType base_type = p_base.type; - bool _static = base_type.is_meta_type; - - if (_static && base_type.kind != GDScriptParser::DataType::BUILTIN) { - ScriptCodeCompletionOption option("new", ScriptCodeCompletionOption::KIND_FUNCTION); - option.insert_text += "("; - r_result.insert(option.display, option); - } - - while (base_type.has_type) { - switch (base_type.kind) { - case GDScriptParser::DataType::CLASS: { - GDScriptCompletionContext c = p_context; - c._class = base_type.class_type; - c.block = nullptr; - c.function = nullptr; - _find_identifiers_in_class(c, _static, p_only_functions, false, r_result); - base_type = base_type.class_type->base_type; - } break; - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> script = base_type.script_type; - if (script.is_valid()) { - if (!_static && !p_only_functions) { - if (p_context.base && p_context.base->get_script_instance()) { - List<PropertyInfo> members; - p_context.base->get_script_instance()->get_property_list(&members); - for (List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - for (const Set<StringName>::Element *E = script->get_members().front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().operator String(), ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - if (!p_only_functions) { - for (const Map<StringName, Variant>::Element *E = script->get_constants().front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CONSTANT); - r_result.insert(option.display, option); - } - } - for (const Map<StringName, GDScriptFunction *>::Element *E = script->get_member_functions().front(); E; E = E->next()) { - if (!_static || E->get()->is_static()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_FUNCTION); - if (E->get()->get_argument_count()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - } - if (!p_only_functions) { - for (const Map<StringName, Ref<GDScript>>::Element *E = script->get_subclasses().front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - r_result.insert(option.display, option); - } - } - base_type = GDScriptParser::DataType(); - if (script->get_base().is_valid()) { - base_type.has_type = true; - base_type.kind = GDScriptParser::DataType::GDSCRIPT; - base_type.script_type = script->get_base(); - } else { - base_type.has_type = script->get_instance_base_type() != StringName(); - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = script->get_instance_base_type(); - } - } else { - return; - } - } break; - case GDScriptParser::DataType::SCRIPT: { - Ref<Script> scr = base_type.script_type; - if (scr.is_valid()) { - if (!_static && !p_only_functions) { - List<PropertyInfo> members; - scr->get_script_property_list(&members); - for (List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - if (!p_only_functions) { - Map<StringName, Variant> constants; - scr->get_constants(&constants); - for (Map<StringName, Variant>::Element *E = constants.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CONSTANT); - r_result.insert(option.display, option); - } - } - - List<MethodInfo> methods; - scr->get_script_method_list(&methods); - for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); - if (E->get().arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - - Ref<Script> base_script = scr->get_base_script(); - if (base_script.is_valid()) { - base_type.script_type = base_script; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = scr->get_instance_base_type(); - } - } else { - return; - } - } break; - case GDScriptParser::DataType::NATIVE: { - StringName type = base_type.native_type; - if (!ClassDB::class_exists(type)) { - type = String("_") + type; - if (!ClassDB::class_exists(type)) { - return; - } - } - - if (!p_only_functions) { - List<String> constants; - ClassDB::get_integer_constant_list(type, &constants); - for (List<String>::Element *E = constants.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CONSTANT); - r_result.insert(option.display, option); - } - - if (!_static) { - List<PropertyInfo> pinfo; - ClassDB::get_property_list(type, &pinfo); - for (List<PropertyInfo>::Element *E = pinfo.front(); E; E = E->next()) { - if (E->get().usage & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_CATEGORY)) { - continue; - } - if (E->get().name.find("/") != -1) { - continue; - } - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - } - - if (!_static) { - List<MethodInfo> methods; - bool is_autocompleting_getters = GLOBAL_GET("debug/gdscript/completion/autocomplete_setters_and_getters").booleanize(); - ClassDB::get_method_list(type, &methods, false, !is_autocompleting_getters); - for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().name.begins_with("_")) { - continue; - } - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); - if (E->get().arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - } - - return; - } break; - case GDScriptParser::DataType::BUILTIN: { - Callable::CallError err; - Variant tmp = Variant::construct(base_type.builtin_type, nullptr, 0, err); - if (err.error != Callable::CallError::CALL_OK) { - return; - } - - if (!p_only_functions) { - List<PropertyInfo> members; - if (p_base.value.get_type() != Variant::NIL) { - p_base.value.get_property_list(&members); - } else { - tmp.get_property_list(&members); - } - - for (List<PropertyInfo>::Element *E = members.front(); E; E = E->next()) { - if (String(E->get().name).find("/") == -1) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_MEMBER); - r_result.insert(option.display, option); - } - } - } - - List<MethodInfo> methods; - tmp.get_method_list(&methods); - for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_FUNCTION); - if (E->get().arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - - return; - } break; - default: { - return; - } break; - } - } -} - -static void _find_identifiers(const GDScriptCompletionContext &p_context, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { - const GDScriptParser::BlockNode *block = p_context.block; - - if (p_context.function) { - const GDScriptParser::FunctionNode *f = p_context.function; - - for (int i = 0; i < f->arguments.size(); i++) { - ScriptCodeCompletionOption option(f->arguments[i].operator String(), ScriptCodeCompletionOption::KIND_PLAIN_TEXT); - r_result.insert(option.display, option); - } - } - - if (!p_only_functions && block) { - GDScriptCompletionContext c = p_context; - c.block = block; - _find_identifiers_in_block(c, r_result); - } - - const GDScriptParser::ClassNode *clss = p_context._class; - bool _static = p_context.function && p_context.function->_static; - - while (clss) { - GDScriptCompletionContext c = p_context; - c._class = clss; - c.block = nullptr; - c.function = nullptr; - _find_identifiers_in_class(c, _static, p_only_functions, false, r_result); - _static = true; - clss = clss->owner; - } - - for (int i = 0; i < GDScriptFunctions::FUNC_MAX; i++) { - MethodInfo mi = GDScriptFunctions::get_info(GDScriptFunctions::Function(i)); - ScriptCodeCompletionOption option(String(GDScriptFunctions::get_func_name(GDScriptFunctions::Function(i))), ScriptCodeCompletionOption::KIND_FUNCTION); - if (mi.arguments.size() || (mi.flags & METHOD_FLAG_VARARG)) { - option.insert_text += "("; - } else { - option.insert_text += "()"; - } - r_result.insert(option.display, option); - } - - static const char *_type_names[Variant::VARIANT_MAX] = { - "null", "bool", "int", "float", "String", "Vector2", "Vector2i", "Rect2", "Rect2i", "Vector3", "Vector3i", "Transform2D", "Plane", "Quat", "AABB", "Basis", "Transform", - "Color", "StringName", "NodePath", "RID", "Object", "Callable", "Signal", "Dictionary", "Array", "PackedByteArray", "PackedInt32Array", "PackedInt64Array", "PackedFloat32Array", "PackedFloat64Array", "PackedStringArray", - "PackedVector2Array", "PackedVector3Array", "PackedColorArray" - }; - - for (int i = 0; i < Variant::VARIANT_MAX; i++) { - ScriptCodeCompletionOption option(_type_names[i], ScriptCodeCompletionOption::KIND_CLASS); - r_result.insert(option.display, option); - } - - static const char *_keywords[] = { - "and", "in", "not", "or", "false", "PI", "TAU", "INF", "NAN", "self", "true", "as", "assert", - "breakpoint", "class", "extends", "is", "func", "preload", "setget", "signal", "tool", "yield", - "const", "enum", "export", "onready", "static", "var", "break", "continue", "if", "elif", - "else", "for", "pass", "return", "match", "while", "remote", "master", "puppet", - "remotesync", "mastersync", "puppetsync", - nullptr - }; - - const char **kw = _keywords; - while (*kw) { - ScriptCodeCompletionOption option(*kw, ScriptCodeCompletionOption::KIND_PLAIN_TEXT); - r_result.insert(option.display, option); - kw++; - } - - // Autoload singletons - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - String path = ProjectSettings::get_singleton()->get(s); - if (path.begins_with("*")) { - ScriptCodeCompletionOption option(s.get_slice("/", 1), ScriptCodeCompletionOption::KIND_CONSTANT); - r_result.insert(option.display, option); - } - } - - // Named scripts - List<StringName> named_scripts; - ScriptServer::get_global_class_list(&named_scripts); - for (List<StringName>::Element *E = named_scripts.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - r_result.insert(option.display, option); - } - - // Native classes - for (const Map<StringName, int>::Element *E = GDScriptLanguage::get_singleton()->get_global_map().front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - r_result.insert(option.display, option); - } -} - -static void _find_call_arguments(const GDScriptCompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, int p_argidx, bool p_static, Map<String, ScriptCodeCompletionOption> &r_result, String &r_arghint) { +static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, const GDScriptCompletionIdentifier &p_base, const StringName &p_method, int p_argidx, bool p_static, Map<String, ScriptCodeCompletionOption> &r_result, String &r_arghint) { Variant base = p_base.value; GDScriptParser::DataType base_type = p_base.type; const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", false) ? "'" : "\""; -#define IS_METHOD_SIGNAL(m_method) (m_method == "connect" || m_method == "disconnect" || m_method == "is_connected" || m_method == "emit_signal") - - while (base_type.has_type) { + while (base_type.is_set() && !base_type.is_variant()) { switch (base_type.kind) { case GDScriptParser::DataType::CLASS: { - for (int i = 0; i < base_type.class_type->static_functions.size(); i++) { - if (base_type.class_type->static_functions[i]->name == p_method) { - r_arghint = _make_arguments_hint(base_type.class_type->static_functions[i], p_argidx); - return; - } - } - for (int i = 0; i < base_type.class_type->functions.size(); i++) { - if (base_type.class_type->functions[i]->name == p_method) { - r_arghint = _make_arguments_hint(base_type.class_type->functions[i], p_argidx); - return; - } - } + if (base_type.class_type->has_member(p_method)) { + const GDScriptParser::ClassNode::Member &member = base_type.class_type->get_member(p_method); - if (IS_METHOD_SIGNAL(p_method) && p_argidx == 0) { - for (int i = 0; i < base_type.class_type->_signals.size(); i++) { - ScriptCodeCompletionOption option(base_type.class_type->_signals[i].name.operator String(), ScriptCodeCompletionOption::KIND_SIGNAL); - option.insert_text = quote_style + option.display + quote_style; - r_result.insert(option.display, option); + if (member.type == GDScriptParser::ClassNode::Member::FUNCTION) { + r_arghint = _make_arguments_hint(member.function, p_argidx); + return; } } base_type = base_type.class_type->base_type; } break; - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> gds = base_type.script_type; - if (gds.is_valid()) { - if (IS_METHOD_SIGNAL(p_method) && p_argidx == 0) { - List<MethodInfo> signals; - gds->get_script_signal_list(&signals); - for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_SIGNAL); - option.insert_text = quote_style + option.display + quote_style; - r_result.insert(option.display, option); - } - } - Ref<GDScript> base_script = gds->get_base_script(); - if (base_script.is_valid()) { - base_type.script_type = base_script; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = gds->get_instance_base_type(); - } - } else { - return; - } - } break; case GDScriptParser::DataType::NATIVE: { - StringName class_name = base_type.native_type; + StringName class_name = _get_real_class_name(base_type.native_type); if (!ClassDB::class_exists(class_name)) { - class_name = String("_") + class_name; - if (!ClassDB::class_exists(class_name)) { - base_type.has_type = false; - break; - } + base_type.kind = GDScriptParser::DataType::UNRESOLVED; + break; } - List<MethodInfo> methods; - ClassDB::get_method_list(class_name, &methods); - ClassDB::get_virtual_methods(class_name, &methods); + MethodInfo info; int method_args = 0; - for (List<MethodInfo>::Element *E = methods.front(); E; E = E->next()) { - if (E->get().name == p_method) { - method_args = E->get().arguments.size(); - if (base.get_type() == Variant::OBJECT) { - Object *obj = base.operator Object *(); - if (obj) { - List<String> options; - obj->get_argument_options(p_method, p_argidx, &options); - for (List<String>::Element *F = options.front(); F; F = F->next()) { - ScriptCodeCompletionOption option(F->get(), ScriptCodeCompletionOption::KIND_FUNCTION); - r_result.insert(option.display, option); - } + if (ClassDB::get_method_info(class_name, p_method, &info)) { + method_args = info.arguments.size(); + if (base.get_type() == Variant::OBJECT) { + Object *obj = base.operator Object *(); + if (obj) { + List<String> options; + obj->get_argument_options(p_method, p_argidx, &options); + for (List<String>::Element *F = options.front(); F; F = F->next()) { + ScriptCodeCompletionOption option(F->get(), ScriptCodeCompletionOption::KIND_FUNCTION); + r_result.insert(option.display, option); } } + } - if (p_argidx < method_args) { - PropertyInfo arg_info = E->get().arguments[p_argidx]; - if (arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) { - _find_enumeration_candidates(arg_info.class_name, r_result); - } + if (p_argidx < method_args) { + PropertyInfo arg_info = info.arguments[p_argidx]; + if (arg_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) { + _find_enumeration_candidates(p_context, arg_info.class_name, r_result); } - - r_arghint = _make_arguments_hint(E->get(), p_argidx); - break; } - } - if (IS_METHOD_SIGNAL(p_method) && p_argidx == 0) { - List<MethodInfo> signals; - ClassDB::get_signal_list(class_name, &signals); - for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().name, ScriptCodeCompletionOption::KIND_SIGNAL); - option.insert_text = quote_style + option.display + quote_style; - r_result.insert(option.display, option); - } + r_arghint = _make_arguments_hint(info, p_argidx); + return; } -#undef IS_METHOD_SIGNAL if (ClassDB::is_parent_class(class_name, "Node") && (p_method == "get_node" || p_method == "has_node") && p_argidx == 0) { // Get autoloads @@ -2359,7 +2244,7 @@ static void _find_call_arguments(const GDScriptCompletionContext &p_context, con } } - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; case GDScriptParser::DataType::BUILTIN: { if (base.get_type() == Variant::NIL) { @@ -2379,139 +2264,93 @@ static void _find_call_arguments(const GDScriptCompletionContext &p_context, con } } - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; default: { - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; } } } -static void _find_call_arguments(GDScriptCompletionContext &p_context, const GDScriptParser::Node *p_node, int p_argidx, Map<String, ScriptCodeCompletionOption> &r_result, bool &r_forced, String &r_arghint) { +static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, const GDScriptParser::Node *p_call, int p_argidx, Map<String, ScriptCodeCompletionOption> &r_result, bool &r_forced, String &r_arghint) { const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", false) ? "'" : "\""; - if (!p_node || p_node->type != GDScriptParser::Node::TYPE_OPERATOR) { + if (p_call->type == GDScriptParser::Node::PRELOAD) { + if (p_argidx == 0 && bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) { + _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), r_result); + } + + MethodInfo mi(PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "Resource"), "preload", PropertyInfo(Variant::STRING, "path")); + r_arghint = _make_arguments_hint(mi, p_argidx); + return; + } else if (p_call->type != GDScriptParser::Node::CALL) { return; } Variant base; GDScriptParser::DataType base_type; - StringName function; bool _static = false; - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(p_node); + const GDScriptParser::CallNode *call = static_cast<const GDScriptParser::CallNode *>(p_call); + GDScriptParser::Node::Type callee_type = GDScriptParser::Node::NONE; GDScriptCompletionIdentifier connect_base; - if (op->op != GDScriptParser::OperatorNode::OP_CALL && op->op != GDScriptParser::OperatorNode::OP_PARENT_CALL) { - return; - } + if (GDScriptParser::get_builtin_function(call->function_name) < GDScriptFunctions::FUNC_MAX) { + MethodInfo info = GDScriptFunctions::get_info(GDScriptParser::get_builtin_function(call->function_name)); - if (!op->arguments.size()) { - return; - } - - if (op->op == GDScriptParser::OperatorNode::OP_CALL) { - if (op->arguments[0]->type == GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION) { - // Complete built-in function - const GDScriptParser::BuiltInFunctionNode *fn = static_cast<const GDScriptParser::BuiltInFunctionNode *>(op->arguments[0]); - MethodInfo mi = GDScriptFunctions::get_info(fn->function); - - if ((mi.name == "load" || mi.name == "preload") && bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) { - _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), r_result); - } - - r_arghint = _make_arguments_hint(mi, p_argidx); - return; - - } else if (op->arguments[0]->type == GDScriptParser::Node::TYPE_TYPE) { - // Complete constructor - const GDScriptParser::TypeNode *tn = static_cast<const GDScriptParser::TypeNode *>(op->arguments[0]); - - List<MethodInfo> constructors; - Variant::get_constructor_list(tn->vtype, &constructors); + if ((info.name == "load" || info.name == "preload") && bool(EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths"))) { + _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), r_result); + } - int i = 0; - for (List<MethodInfo>::Element *E = constructors.front(); E; E = E->next()) { - if (p_argidx >= E->get().arguments.size()) { - continue; - } - if (i > 0) { - r_arghint += "\n"; - } - r_arghint += _make_arguments_hint(E->get(), p_argidx); - i++; - } - return; - } else if (op->arguments[0]->type == GDScriptParser::Node::TYPE_SELF) { - if (op->arguments.size() < 2 || op->arguments[1]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - return; + r_arghint = _make_arguments_hint(info, p_argidx); + return; + } else if (GDScriptParser::get_builtin_type(call->function_name) < Variant::VARIANT_MAX) { + // Complete constructor + List<MethodInfo> constructors; + Variant::get_constructor_list(GDScriptParser::get_builtin_type(call->function_name), &constructors); + + int i = 0; + for (List<MethodInfo>::Element *E = constructors.front(); E; E = E->next()) { + if (p_argidx >= E->get().arguments.size()) { + continue; } - - base = p_context.base; - - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[1]); - function = id->name; - base_type.has_type = true; - base_type.kind = GDScriptParser::DataType::CLASS; - base_type.class_type = const_cast<GDScriptParser::ClassNode *>(p_context._class); - _static = p_context.function && p_context.function->_static; - - if (function == "connect" && op->arguments.size() >= 4) { - _guess_expression_type(p_context, op->arguments[3], connect_base); + if (i > 0) { + r_arghint += "\n"; } + r_arghint += _make_arguments_hint(E->get(), p_argidx); + i++; + } + return; + } else if (call->is_super || callee_type == GDScriptParser::Node::IDENTIFIER) { + base = p_context.base; - } else { - if (op->arguments.size() < 2 || op->arguments[1]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - return; - } - const GDScriptParser::IdentifierNode *id = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[1]); - function = id->name; + if (p_context.current_class) { + base_type = p_context.current_class->get_datatype(); + _static = !p_context.current_function || p_context.current_function->is_static; + } + } else if (callee_type == GDScriptParser::Node::SUBSCRIPT) { + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(call->callee); + if (subscript->is_attribute) { GDScriptCompletionIdentifier ci; - if (_guess_expression_type(p_context, op->arguments[0], ci)) { + if (_guess_expression_type(p_context, subscript->base, ci)) { base_type = ci.type; base = ci.value; } else { return; } - _static = ci.type.is_meta_type; - if (function == "connect" && op->arguments.size() >= 4) { - _guess_expression_type(p_context, op->arguments[3], connect_base); - } + _static = base_type.is_meta_type; } } else { - if (!p_context._class || op->arguments.size() < 1 || op->arguments[0]->type != GDScriptParser::Node::TYPE_IDENTIFIER) { - return; - } - base_type.has_type = true; - base_type.kind = GDScriptParser::DataType::CLASS; - base_type.class_type = const_cast<GDScriptParser::ClassNode *>(p_context._class); - base_type.is_meta_type = p_context.function && p_context.function->_static; - base = p_context.base; - - function = static_cast<const GDScriptParser::IdentifierNode *>(op->arguments[0])->name; - - if (function == "connect" && op->arguments.size() >= 4) { - _guess_expression_type(p_context, op->arguments[3], connect_base); - } + return; } GDScriptCompletionIdentifier ci; ci.type = base_type; ci.value = base; - _find_call_arguments(p_context, ci, function, p_argidx, _static, r_result, r_arghint); - - if (function == "connect" && p_argidx == 2) { - Map<String, ScriptCodeCompletionOption> methods; - _find_identifiers_in_base(p_context, connect_base, true, methods); - for (Map<String, ScriptCodeCompletionOption>::Element *E = methods.front(); E; E = E->next()) { - ScriptCodeCompletionOption &option = E->value(); - option.insert_text = quote_style + option.display + quote_style; - r_result.insert(option.display, option); - } - } + _find_call_arguments(p_context, ci, call->function_name, p_argidx, _static, r_result, r_arghint); r_forced = r_result.size() > 0; } @@ -2520,150 +2359,213 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", false) ? "'" : "\""; GDScriptParser parser; + GDScriptAnalyzer analyzer(&parser); + + parser.parse(p_code, p_path, true); + analyzer.analyze(); - parser.parse(p_code, p_path.get_base_dir(), false, p_path, true); r_forced = false; Map<String, ScriptCodeCompletionOption> options; - GDScriptCompletionContext context; - context._class = parser.get_completion_class(); - context.block = parser.get_completion_block(); - context.function = parser.get_completion_function(); - context.line = parser.get_completion_line(); - - if (!context._class || context._class->owner == nullptr) { - context.base = p_owner; - context.base_path = p_path.get_base_dir(); - } + GDScriptParser::CompletionContext completion_context = parser.get_completion_context(); + completion_context.base = p_owner; bool is_function = false; - switch (parser.get_completion_type()) { - case GDScriptParser::COMPLETION_NONE: { + switch (completion_context.type) { + case GDScriptParser::COMPLETION_NONE: + break; + case GDScriptParser::COMPLETION_ANNOTATION: { + List<MethodInfo> annotations; + parser.get_annotation_list(&annotations); + for (const List<MethodInfo>::Element *E = annotations.front(); E != nullptr; E = E->next()) { + ScriptCodeCompletionOption option(E->get().name.substr(1), ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + if (E->get().arguments.size() > 0) { + option.insert_text += "("; + } + options.insert(option.display, option); + } + r_forced = true; + } break; + case GDScriptParser::COMPLETION_ANNOTATION_ARGUMENTS: { + if (completion_context.node == nullptr || completion_context.node->type != GDScriptParser::Node::ANNOTATION) { + break; + } + const GDScriptParser::AnnotationNode *annotation = static_cast<const GDScriptParser::AnnotationNode *>(completion_context.node); + _find_annotation_arguments(annotation, completion_context.current_argument, quote_style, options); + r_forced = true; } break; case GDScriptParser::COMPLETION_BUILT_IN_TYPE_CONSTANT: { List<StringName> constants; - Variant::get_constants_for_type(parser.get_completion_built_in_constant(), &constants); - for (List<StringName>::Element *E = constants.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().operator String(), ScriptCodeCompletionOption::KIND_CONSTANT); + Variant::get_constants_for_type(completion_context.builtin_type, &constants); + for (const List<StringName>::Element *E = constants.front(); E != nullptr; E = E->next()) { + ScriptCodeCompletionOption option(E->get(), ScriptCodeCompletionOption::KIND_CONSTANT); options.insert(option.display, option); } } break; - case GDScriptParser::COMPLETION_PARENT_FUNCTION: { - _find_identifiers_in_class(context, !context.function || context.function->_static, true, true, options); + case GDScriptParser::COMPLETION_INHERIT_TYPE: { + _list_available_types(true, completion_context, options); + r_forced = true; } break; - case GDScriptParser::COMPLETION_FUNCTION: { - is_function = true; - [[fallthrough]]; + case GDScriptParser::COMPLETION_TYPE_NAME_OR_VOID: { + ScriptCodeCompletionOption option("void", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + options.insert(option.display, option); } - case GDScriptParser::COMPLETION_IDENTIFIER: { - _find_identifiers(context, is_function, options); + [[fallthrough]]; + case GDScriptParser::COMPLETION_TYPE_NAME: { + _list_available_types(false, completion_context, options); + r_forced = true; } break; - case GDScriptParser::COMPLETION_GET_NODE: { - if (p_owner) { - List<String> opts; - p_owner->get_argument_options("get_node", 0, &opts); - - for (List<String>::Element *E = opts.front(); E; E = E->next()) { - String opt = E->get().strip_edges(); - if (opt.is_quoted()) { - r_forced = true; - String idopt = opt.unquote(); - if (idopt.replace("/", "_").is_valid_identifier()) { - ScriptCodeCompletionOption option(idopt, ScriptCodeCompletionOption::KIND_NODE_PATH); - options.insert(option.display, option); - } else { - ScriptCodeCompletionOption option(opt, ScriptCodeCompletionOption::KIND_NODE_PATH); - options.insert(option.display, option); - } - } + case GDScriptParser::COMPLETION_PROPERTY_DECLARATION_OR_TYPE: { + _list_available_types(false, completion_context, options); + ScriptCodeCompletionOption get("get", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + options.insert(get.display, get); + ScriptCodeCompletionOption set("set", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + options.insert(set.display, set); + r_forced = true; + } break; + case GDScriptParser::COMPLETION_PROPERTY_DECLARATION: { + ScriptCodeCompletionOption get("get", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + options.insert(get.display, get); + ScriptCodeCompletionOption set("set", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); + options.insert(set.display, set); + r_forced = true; + } break; + case GDScriptParser::COMPLETION_PROPERTY_METHOD: { + if (!completion_context.current_class) { + break; + } + for (int i = 0; i < completion_context.current_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &member = completion_context.current_class->members[i]; + if (member.type != GDScriptParser::ClassNode::Member::FUNCTION) { + continue; } - - // Get autoloads - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - String name = s.get_slice("/", 1); - ScriptCodeCompletionOption option(quote_style + "/root/" + name + quote_style, ScriptCodeCompletionOption::KIND_NODE_PATH); - options.insert(option.display, option); + if (member.function->is_static) { + continue; } + ScriptCodeCompletionOption option(member.function->identifier->name, ScriptCodeCompletionOption::KIND_FUNCTION); + options.insert(option.display, option); } + r_forced = true; } break; - case GDScriptParser::COMPLETION_METHOD: { - is_function = true; - [[fallthrough]]; - } - case GDScriptParser::COMPLETION_INDEX: { - const GDScriptParser::Node *node = parser.get_completion_node(); - if (node->type != GDScriptParser::Node::TYPE_OPERATOR) { + case GDScriptParser::COMPLETION_ASSIGN: { + GDScriptCompletionIdentifier type; + if (!completion_context.node || completion_context.node->type != GDScriptParser::Node::ASSIGNMENT) { break; } - const GDScriptParser::OperatorNode *op = static_cast<const GDScriptParser::OperatorNode *>(node); - if (op->arguments.size() < 1) { + if (!_guess_expression_type(completion_context, static_cast<const GDScriptParser::AssignmentNode *>(completion_context.node)->assignee, type)) { + _find_identifiers(completion_context, false, options); + r_forced = true; break; } + if (!type.enumeration.empty()) { + _find_enumeration_candidates(completion_context, type.enumeration, options); + r_forced = options.size() > 0; + } else { + _find_identifiers(completion_context, false, options); + r_forced = true; + } + } break; + case GDScriptParser::COMPLETION_METHOD: + is_function = true; + [[fallthrough]]; + case GDScriptParser::COMPLETION_IDENTIFIER: { + _find_identifiers(completion_context, is_function, options); + } break; + case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD: + is_function = true; + [[fallthrough]]; + case GDScriptParser::COMPLETION_ATTRIBUTE: { + r_forced = true; + const GDScriptParser::SubscriptNode *attr = static_cast<const GDScriptParser::SubscriptNode *>(completion_context.node); + if (attr->base) { + GDScriptCompletionIdentifier base; + if (!_guess_expression_type(completion_context, attr->base, base)) { + break; + } + + _find_identifiers_in_base(base, is_function, options); + } + } break; + case GDScriptParser::COMPLETION_SUBSCRIPT: { + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(completion_context.node); GDScriptCompletionIdentifier base; - if (!_guess_expression_type(context, op->arguments[0], base)) { + if (!_guess_expression_type(completion_context, subscript->base, base)) { break; } - GDScriptCompletionContext c = context; - c.function = nullptr; - c.block = nullptr; + GDScriptParser::CompletionContext c = completion_context; + c.current_function = nullptr; + c.current_suite = nullptr; c.base = base.value.get_type() == Variant::OBJECT ? base.value.operator Object *() : nullptr; if (base.type.kind == GDScriptParser::DataType::CLASS) { - c._class = base.type.class_type; + c.current_class = base.type.class_type; } else { - c._class = nullptr; + c.current_class = nullptr; + } + + _find_identifiers_in_base(base, false, options); + } break; + case GDScriptParser::COMPLETION_TYPE_ATTRIBUTE: { + if (!completion_context.current_class) { + break; + } + const GDScriptParser::TypeNode *type = static_cast<const GDScriptParser::TypeNode *>(completion_context.node); + bool found = true; + GDScriptCompletionIdentifier base; + base.type.kind = GDScriptParser::DataType::CLASS; + base.type.type_source = GDScriptParser::DataType::INFERRED; + base.type.is_constant = true; + base.type.class_type = completion_context.current_class; + base.value = completion_context.base; + + for (int i = 0; i < completion_context.current_argument; i++) { + GDScriptCompletionIdentifier ci; + if (!_guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci)) { + found = false; + break; + } + base = ci; } - _find_identifiers_in_base(c, base, is_function, options); + // TODO: Improve this to only list types. + if (found) { + _find_identifiers_in_base(base, false, options); + } + r_forced = true; + } break; + case GDScriptParser::COMPLETION_RESOURCE_PATH: { + if (EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths")) { + _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), options); + r_forced = true; + } } break; case GDScriptParser::COMPLETION_CALL_ARGUMENTS: { - _find_call_arguments(context, parser.get_completion_node(), parser.get_completion_argument_index(), options, r_forced, r_call_hint); + if (!completion_context.node) { + break; + } + _find_call_arguments(completion_context, completion_context.node, completion_context.current_argument, options, r_forced, r_call_hint); } break; - case GDScriptParser::COMPLETION_VIRTUAL_FUNC: { - GDScriptParser::DataType native_type = context._class->base_type; - while (native_type.has_type && native_type.kind != GDScriptParser::DataType::NATIVE) { + case GDScriptParser::COMPLETION_OVERRIDE_METHOD: { + GDScriptParser::DataType native_type = completion_context.current_class->base_type; + while (native_type.is_set() && native_type.kind != GDScriptParser::DataType::NATIVE) { switch (native_type.kind) { case GDScriptParser::DataType::CLASS: { native_type = native_type.class_type->base_type; } break; - case GDScriptParser::DataType::GDSCRIPT: { - Ref<GDScript> gds = native_type.script_type; - if (gds.is_valid()) { - Ref<GDScript> base = gds->get_base_script(); - if (base.is_valid()) { - native_type.script_type = base; - } else { - native_type.native_type = gds->get_instance_base_type(); - native_type.kind = GDScriptParser::DataType::NATIVE; - } - } else { - native_type.has_type = false; - } - } break; default: { - native_type.has_type = false; + native_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; } } - if (!native_type.has_type) { + if (!native_type.is_set()) { break; } - StringName class_name = native_type.native_type; + StringName class_name = _get_real_class_name(native_type.native_type); if (!ClassDB::class_exists(class_name)) { - class_name = String("_") + class_name; - if (!ClassDB::class_exists(class_name)) { - break; - } + break; } bool use_type_hint = EditorSettings::get_singleton()->get_setting("text_editor/completion/add_type_hints").operator bool(); @@ -2715,245 +2617,41 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path options.insert(option.display, option); } } break; - case GDScriptParser::COMPLETION_YIELD: { - const GDScriptParser::Node *node = parser.get_completion_node(); - - GDScriptCompletionContext c = context; - c.line = node->line; - GDScriptCompletionIdentifier type; - if (!_guess_expression_type(c, node, type)) { - break; - } + case GDScriptParser::COMPLETION_GET_NODE: { + if (p_owner) { + List<String> opts; + p_owner->get_argument_options("get_node", 0, &opts); - GDScriptParser::DataType base_type = type.type; - while (base_type.has_type) { - switch (base_type.kind) { - case GDScriptParser::DataType::CLASS: { - for (int i = 0; i < base_type.class_type->_signals.size(); i++) { - ScriptCodeCompletionOption option(base_type.class_type->_signals[i].name.operator String(), ScriptCodeCompletionOption::KIND_SIGNAL); - option.insert_text = quote_style + option.display + quote_style; + for (List<String>::Element *E = opts.front(); E; E = E->next()) { + String opt = E->get().strip_edges(); + if (opt.is_quoted()) { + r_forced = true; + String idopt = opt.unquote(); + if (idopt.replace("/", "_").is_valid_identifier()) { + ScriptCodeCompletionOption option(idopt, ScriptCodeCompletionOption::KIND_NODE_PATH); options.insert(option.display, option); - } - base_type = base_type.class_type->base_type; - } break; - case GDScriptParser::DataType::SCRIPT: - case GDScriptParser::DataType::GDSCRIPT: { - Ref<Script> scr = base_type.script_type; - if (scr.is_valid()) { - List<MethodInfo> signals; - scr->get_script_signal_list(&signals); - for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(quote_style + E->get().name + quote_style, ScriptCodeCompletionOption::KIND_SIGNAL); - options.insert(option.display, option); - } - Ref<Script> base_script = scr->get_base_script(); - if (base_script.is_valid()) { - base_type.script_type = base_script; - } else { - base_type.kind = GDScriptParser::DataType::NATIVE; - base_type.native_type = scr->get_instance_base_type(); - } } else { - base_type.has_type = false; - } - } break; - case GDScriptParser::DataType::NATIVE: { - base_type.has_type = false; - - StringName class_name = base_type.native_type; - if (!ClassDB::class_exists(class_name)) { - class_name = String("_") + class_name; - if (!ClassDB::class_exists(class_name)) { - break; - } - } - - List<MethodInfo> signals; - ClassDB::get_signal_list(class_name, &signals); - for (List<MethodInfo>::Element *E = signals.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(quote_style + E->get().name + quote_style, ScriptCodeCompletionOption::KIND_SIGNAL); + ScriptCodeCompletionOption option(opt, ScriptCodeCompletionOption::KIND_NODE_PATH); options.insert(option.display, option); } - } break; - default: { - base_type.has_type = false; } } - } - } break; - case GDScriptParser::COMPLETION_RESOURCE_PATH: { - if (EditorSettings::get_singleton()->get("text_editor/completion/complete_file_paths")) { - _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), options); - r_forced = true; - } - } break; - case GDScriptParser::COMPLETION_ASSIGN: { - GDScriptCompletionIdentifier type; - if (!_guess_expression_type(context, parser.get_completion_node(), type)) { - break; - } - if (!type.enumeration.empty()) { - _find_enumeration_candidates(type.enumeration, options); - r_forced = options.size() > 0; - } - } break; - case GDScriptParser::COMPLETION_TYPE_HINT: { - const GDScriptParser::ClassNode *clss = context._class; - while (clss) { - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = clss->constant_expressions.front(); E; E = E->next()) { - GDScriptCompletionIdentifier constant; - GDScriptCompletionContext c = context; - c.function = nullptr; - c.block = nullptr; - c.line = E->value().expression->line; - if (_guess_expression_type(c, E->value().expression, constant)) { - if (constant.type.has_type && constant.type.is_meta_type) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } - } - for (int i = 0; i < clss->subclasses.size(); i++) { - if (clss->subclasses[i]->name != StringName()) { - ScriptCodeCompletionOption option(clss->subclasses[i]->name.operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } - clss = clss->owner; - for (int i = 0; i < Variant::VARIANT_MAX; i++) { - ScriptCodeCompletionOption option(Variant::get_type_name((Variant::Type)i), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - ScriptCodeCompletionOption option(s.get_slice("/", 1), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } + // Get autoloads. + Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list(); - List<StringName> native_classes; - ClassDB::get_class_list(&native_classes); - for (List<StringName>::Element *E = native_classes.front(); E; E = E->next()) { - String class_name = E->get().operator String(); - if (class_name.begins_with("_")) { - class_name = class_name.right(1); - } - if (Engine::get_singleton()->has_singleton(class_name)) { - continue; + for (Map<StringName, ProjectSettings::AutoloadInfo>::Element *E = autoloads.front(); E; E = E->next()) { + String name = E->key(); + ScriptCodeCompletionOption option(quote_style + "/root/" + name + quote_style, ScriptCodeCompletionOption::KIND_NODE_PATH); + options.insert(option.display, option); } - ScriptCodeCompletionOption option(class_name, ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - - // Named scripts - List<StringName> named_scripts; - ScriptServer::get_global_class_list(&named_scripts); - for (List<StringName>::Element *E = named_scripts.front(); E; E = E->next()) { - ScriptCodeCompletionOption option(E->get().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - - if (parser.get_completion_identifier_is_function()) { - ScriptCodeCompletionOption option("void", ScriptCodeCompletionOption::KIND_PLAIN_TEXT); - options.insert(option.display, option); } - r_forced = true; } break; - case GDScriptParser::COMPLETION_TYPE_HINT_INDEX: { - GDScriptCompletionIdentifier base; - String index = parser.get_completion_cursor().operator String(); - if (!_guess_identifier_type(context, index.get_slice(".", 0), base)) { + case GDScriptParser::COMPLETION_SUPER_METHOD: { + if (!completion_context.current_class) { break; } - - GDScriptCompletionContext c = context; - c._class = nullptr; - c.function = nullptr; - c.block = nullptr; - bool finding = true; - index = index.right(index.find(".") + 1); - while (index.find(".") != -1) { - String id = index.get_slice(".", 0); - - GDScriptCompletionIdentifier sub_base; - if (!_guess_identifier_type_from_base(c, base, id, sub_base)) { - finding = false; - break; - } - index = index.right(index.find(".") + 1); - base = sub_base; - } - - if (!finding) { - break; - } - - GDScriptParser::DataType base_type = base.type; - while (base_type.has_type) { - switch (base_type.kind) { - case GDScriptParser::DataType::CLASS: { - if (base_type.class_type) { - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = base_type.class_type->constant_expressions.front(); E; E = E->next()) { - GDScriptCompletionIdentifier constant; - GDScriptCompletionContext c2 = context; - c2._class = base_type.class_type; - c2.function = nullptr; - c2.block = nullptr; - c2.line = E->value().expression->line; - if (_guess_expression_type(c2, E->value().expression, constant)) { - if (constant.type.has_type && constant.type.is_meta_type) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } - } - for (int i = 0; i < base_type.class_type->subclasses.size(); i++) { - if (base_type.class_type->subclasses[i]->name != StringName()) { - ScriptCodeCompletionOption option(base_type.class_type->subclasses[i]->name.operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } - - base_type = base_type.class_type->base_type; - } else { - base_type.has_type = false; - } - } break; - case GDScriptParser::DataType::SCRIPT: - case GDScriptParser::DataType::GDSCRIPT: { - Ref<Script> scr = base_type.script_type; - if (scr.is_valid()) { - Map<StringName, Variant> constants; - scr->get_constants(&constants); - for (Map<StringName, Variant>::Element *E = constants.front(); E; E = E->next()) { - Ref<Script> const_scr = E->value(); - if (const_scr.is_valid()) { - ScriptCodeCompletionOption option(E->key().operator String(), ScriptCodeCompletionOption::KIND_CLASS); - options.insert(option.display, option); - } - } - Ref<Script> base_script = scr->get_base_script(); - if (base_script.is_valid()) { - base_type.script_type = base_script; - } else { - base_type.has_type = false; - } - } else { - base_type.has_type = false; - } - } break; - default: { - base_type.has_type = false; - } break; - } - } - r_forced = options.size() > 0; + _find_identifiers_in_class(completion_context.current_class, true, false, true, options); } break; } @@ -3060,53 +2758,19 @@ void GDScriptLanguage::auto_indent_code(String &p_code, int p_from_line, int p_t static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, const String &p_symbol, bool p_is_function, GDScriptLanguage::LookupResult &r_result) { GDScriptParser::DataType base_type = p_base; - while (base_type.has_type) { + while (base_type.is_set()) { switch (base_type.kind) { case GDScriptParser::DataType::CLASS: { if (base_type.class_type) { - if (p_is_function) { - for (int i = 0; i < base_type.class_type->functions.size(); i++) { - if (base_type.class_type->functions[i]->name == p_symbol) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = base_type.class_type->functions[i]->line; - return OK; - } - } - for (int i = 0; i < base_type.class_type->static_functions.size(); i++) { - if (base_type.class_type->static_functions[i]->name == p_symbol) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = base_type.class_type->static_functions[i]->line; - return OK; - } - } - } else { - if (base_type.class_type->constant_expressions.has(p_symbol)) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = base_type.class_type->constant_expressions[p_symbol].expression->line; - return OK; - } - - for (int i = 0; i < base_type.class_type->variables.size(); i++) { - if (base_type.class_type->variables[i].identifier == p_symbol) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = base_type.class_type->variables[i].line; - return OK; - } - } - - for (int i = 0; i < base_type.class_type->subclasses.size(); i++) { - if (base_type.class_type->subclasses[i]->name == p_symbol) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = base_type.class_type->subclasses[i]->line; - return OK; - } - } + if (base_type.class_type->has_member(p_symbol)) { + r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; + r_result.location = base_type.class_type->get_member(p_symbol).get_line(); + return OK; } base_type = base_type.class_type->base_type; } } break; - case GDScriptParser::DataType::SCRIPT: - case GDScriptParser::DataType::GDSCRIPT: { + case GDScriptParser::DataType::SCRIPT: { Ref<Script> scr = base_type.script_type; if (scr.is_valid()) { int line = scr->get_member_line(p_symbol); @@ -3124,17 +2788,14 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co base_type.native_type = scr->get_instance_base_type(); } } else { - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } } break; case GDScriptParser::DataType::NATIVE: { - StringName class_name = base_type.native_type; + StringName class_name = _get_real_class_name(base_type.native_type); if (!ClassDB::class_exists(class_name)) { - class_name = String("_") + class_name; - if (!ClassDB::class_exists(class_name)) { - base_type.has_type = false; - break; - } + base_type.kind = GDScriptParser::DataType::UNRESOLVED; + break; } if (ClassDB::has_method(class_name, p_symbol, true)) { @@ -3174,15 +2835,11 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co } } - List<PropertyInfo> properties; - ClassDB::get_property_list(class_name, &properties, true); - for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().name == p_symbol) { - r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_PROPERTY; - r_result.class_name = base_type.native_type; - r_result.class_member = p_symbol; - return OK; - } + if (ClassDB::has_property(class_name, p_symbol, true)) { + r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_PROPERTY; + r_result.class_name = base_type.native_type; + r_result.class_member = p_symbol; + return OK; } StringName parent = ClassDB::get_parent_class(class_name); @@ -3193,11 +2850,11 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co base_type.native_type = parent; } } else { - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } } break; case GDScriptParser::DataType::BUILTIN: { - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; if (Variant::has_constant(base_type.builtin_type, p_symbol)) { r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_CONSTANT; @@ -3213,7 +2870,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co v = v_ref; } else { Callable::CallError err; - v = Variant::construct(base_type.builtin_type, nullptr, 0, err); + v = Variant::construct(base_type.builtin_type, NULL, 0, err); if (err.error != Callable::CallError::CALL_OK) { break; } @@ -3236,7 +2893,7 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co } } break; default: { - base_type.has_type = false; + base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; } } @@ -3285,26 +2942,18 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol } GDScriptParser parser; - parser.parse(p_code, p_path.get_base_dir(), false, p_path, true); - - if (parser.get_completion_type() == GDScriptParser::COMPLETION_NONE) { - return ERR_CANT_RESOLVE; - } + parser.parse(p_code, p_path, true); + GDScriptAnalyzer analyzer(&parser); + analyzer.analyze(); - GDScriptCompletionContext context; - context._class = parser.get_completion_class(); - context.function = parser.get_completion_function(); - context.block = parser.get_completion_block(); - context.line = parser.get_completion_line(); - context.base = p_owner; - context.base_path = p_path.get_base_dir(); + GDScriptParser::CompletionContext context = parser.get_completion_context(); - if (context._class && context._class->extends_class.size() > 0) { + if (context.current_class && context.current_class->extends.size() > 0) { bool success = false; - ClassDB::get_integer_constant(context._class->extends_class[0], p_symbol, &success); + ClassDB::get_integer_constant(context.current_class->extends[0], p_symbol, &success); if (success) { r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_CONSTANT; - r_result.class_name = context._class->extends_class[0]; + r_result.class_name = context.current_class->extends[0]; r_result.class_member = p_symbol; return OK; } @@ -3312,57 +2961,40 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol bool is_function = false; - switch (parser.get_completion_type()) { + switch (context.type) { case GDScriptParser::COMPLETION_BUILT_IN_TYPE_CONSTANT: { r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_CONSTANT; - r_result.class_name = Variant::get_type_name(parser.get_completion_built_in_constant()); + r_result.class_name = Variant::get_type_name(context.builtin_type); r_result.class_member = p_symbol; return OK; } break; - case GDScriptParser::COMPLETION_PARENT_FUNCTION: - case GDScriptParser::COMPLETION_FUNCTION: { + case GDScriptParser::COMPLETION_SUPER_METHOD: + case GDScriptParser::COMPLETION_METHOD: { is_function = true; [[fallthrough]]; } case GDScriptParser::COMPLETION_IDENTIFIER: { - if (!is_function) { - is_function = parser.get_completion_identifier_is_function(); - } - GDScriptParser::DataType base_type; - if (context._class) { - if (parser.get_completion_type() != GDScriptParser::COMPLETION_PARENT_FUNCTION) { - base_type.has_type = true; - base_type.kind = GDScriptParser::DataType::CLASS; - base_type.class_type = const_cast<GDScriptParser::ClassNode *>(context._class); + if (context.current_class) { + if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) { + base_type = context.current_class->get_datatype(); } else { - base_type = context._class->base_type; + base_type = context.current_class->base_type; } } else { break; } - if (!is_function && context.block) { - // Lookup local variables - const GDScriptParser::BlockNode *block = context.block; - while (block) { - if (block->variables.has(p_symbol)) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = block->variables[p_symbol]->line; - return OK; - } - block = block->parent_block; - } - } - - if (context.function && context.function->name != StringName()) { - // Lookup function arguments - for (int i = 0; i < context.function->arguments.size(); i++) { - if (context.function->arguments[i] == p_symbol) { + if (!is_function && context.current_suite) { + // Lookup local variables. + const GDScriptParser::SuiteNode *suite = context.current_suite; + while (suite) { + if (suite->has_local(p_symbol)) { r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = context.function->line; + r_result.location = suite->get_local(p_symbol).start_line; return OK; } + suite = suite->parent_block; } } @@ -3371,38 +3003,27 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol } if (!is_function) { - // Guess in autoloads as singletons - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - String name = s.get_slice("/", 1); - if (name == String(p_symbol)) { - String path = ProjectSettings::get_singleton()->get(s); - if (path.begins_with("*")) { - String script = path.substr(1, path.length()); - - if (!script.ends_with(".gd")) { - // Not a script, try find the script anyway, - // may have some success - script = script.get_basename() + ".gd"; - } + // Guess in autoloads as singletons. + if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) { + const ProjectSettings::AutoloadInfo &singleton = ProjectSettings::get_singleton()->get_autoload(p_symbol); + if (singleton.is_singleton) { + String script = singleton.path; + if (!script.ends_with(".gd")) { + // Not a script, try find the script anyway, + // may have some success. + script = script.get_basename() + ".gd"; + } - if (FileAccess::exists(script)) { - r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; - r_result.location = 0; - r_result.script = ResourceLoader::load(script); - return OK; - } + if (FileAccess::exists(script)) { + r_result.type = ScriptLanguage::LookupResult::RESULT_SCRIPT_LOCATION; + r_result.location = 0; + r_result.script = ResourceLoader::load(script); + return OK; } } } - // Global + // Global. Map<StringName, int> classes = GDScriptLanguage::get_singleton()->get_global_map(); if (classes.has(p_symbol)) { Variant value = GDScriptLanguage::get_singleton()->get_global_array()[classes[p_symbol]]; @@ -3429,7 +3050,7 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol // We cannot determine the exact nature of the identifier here // Otherwise these codes would work StringName enumName = ClassDB::get_integer_constant_enum("@GlobalScope", p_symbol, true); - if (enumName != nullptr) { + if (enumName != NULL) { r_result.type = ScriptLanguage::LookupResult::RESULT_CLASS_ENUM; r_result.class_name = "@GlobalScope"; r_result.class_member = enumName; @@ -3449,17 +3070,20 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol } } } break; - case GDScriptParser::COMPLETION_METHOD: { + case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD: { is_function = true; [[fallthrough]]; } - case GDScriptParser::COMPLETION_INDEX: { - const GDScriptParser::Node *node = parser.get_completion_node(); - if (node->type != GDScriptParser::Node::TYPE_OPERATOR) { + case GDScriptParser::COMPLETION_ATTRIBUTE: { + if (context.node->type != GDScriptParser::Node::SUBSCRIPT) { + break; + } + const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(context.node); + if (!subscript->is_attribute) { break; } GDScriptCompletionIdentifier base; - if (!_guess_expression_type(context, static_cast<const GDScriptParser::OperatorNode *>(node)->arguments[0], base)) { + if (!_guess_expression_type(context, subscript->base, base)) { break; } @@ -3467,18 +3091,16 @@ Error GDScriptLanguage::lookup_code(const String &p_code, const String &p_symbol return OK; } } break; - case GDScriptParser::COMPLETION_VIRTUAL_FUNC: { - GDScriptParser::DataType base_type = context._class->base_type; + case GDScriptParser::COMPLETION_OVERRIDE_METHOD: { + GDScriptParser::DataType base_type = context.current_class->base_type; if (_lookup_symbol_from_base(base_type, p_symbol, true, r_result) == OK) { return OK; } } break; - case GDScriptParser::COMPLETION_TYPE_HINT: { - GDScriptParser::DataType base_type = context._class->base_type; - base_type.has_type = true; - base_type.kind = GDScriptParser::DataType::CLASS; - base_type.class_type = const_cast<GDScriptParser::ClassNode *>(context._class); + case GDScriptParser::COMPLETION_TYPE_NAME_OR_VOID: + case GDScriptParser::COMPLETION_TYPE_NAME: { + GDScriptParser::DataType base_type = context.current_class->get_datatype(); if (_lookup_symbol_from_base(base_type, p_symbol, false, r_result) == OK) { return OK; diff --git a/modules/gdscript/gdscript_function.cpp b/modules/gdscript/gdscript_function.cpp index 1aab71d161..a4e37a79f8 100644 --- a/modules/gdscript/gdscript_function.cpp +++ b/modules/gdscript/gdscript_function.cpp @@ -210,12 +210,12 @@ String GDScriptFunction::_get_call_error(const Callable::CallError &p_err, const &&OPCODE_CONSTRUCT_DICTIONARY, \ &&OPCODE_CALL, \ &&OPCODE_CALL_RETURN, \ + &&OPCODE_CALL_ASYNC, \ &&OPCODE_CALL_BUILT_IN, \ &&OPCODE_CALL_SELF, \ &&OPCODE_CALL_SELF_BASE, \ - &&OPCODE_YIELD, \ - &&OPCODE_YIELD_SIGNAL, \ - &&OPCODE_YIELD_RESUME, \ + &&OPCODE_AWAIT, \ + &&OPCODE_AWAIT_RESUME, \ &&OPCODE_JUMP, \ &&OPCODE_JUMP_IF, \ &&OPCODE_JUMP_IF_NOT, \ @@ -227,7 +227,8 @@ String GDScriptFunction::_get_call_error(const Callable::CallError &p_err, const &&OPCODE_BREAKPOINT, \ &&OPCODE_LINE, \ &&OPCODE_END \ - }; + }; \ + static_assert((sizeof(switch_table_ops) / sizeof(switch_table_ops[0]) == (OPCODE_END + 1)), "Opcodes in jump table aren't the same as opcodes in enum."); #define OPCODE(m_op) \ m_op: @@ -280,7 +281,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a int line = _initial_line; if (p_state) { - //use existing (supplied) state (yielded) + //use existing (supplied) state (awaited) stack = (Variant *)p_state->stack.ptr(); call_args = (Variant **)&p_state->stack.ptr()[sizeof(Variant) * p_state->stack_size]; //ptr() to avoid bounds check line = p_state->line; @@ -411,7 +412,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a profile.frame_call_count++; } bool exit_ok = false; - bool yielded = false; + bool awaited = false; #endif #ifdef DEBUG_ENABLED @@ -1006,10 +1007,14 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } DISPATCH_OPCODE; + OPCODE(OPCODE_CALL_ASYNC) OPCODE(OPCODE_CALL_RETURN) OPCODE(OPCODE_CALL) { CHECK_SPACE(4); - bool call_ret = _code_ptr[ip] == OPCODE_CALL_RETURN; + bool call_ret = _code_ptr[ip] != OPCODE_CALL; +#ifdef DEBUG_ENABLED + bool call_async = _code_ptr[ip] == OPCODE_CALL_ASYNC; +#endif int argc = _code_ptr[ip + 1]; GET_VARIANT_PTR(base, 2); @@ -1040,6 +1045,22 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (call_ret) { GET_VARIANT_PTR(ret, argc); base->call_ptr(*methodname, (const Variant **)argptrs, argc, ret, err); +#ifdef DEBUG_ENABLED + if (!call_async && ret->get_type() == Variant::OBJECT) { + // Check if getting a function state without await. + bool was_freed = false; + Object *obj = ret->get_validated_object_with_check(was_freed); + + if (was_freed) { + err_text = "Got a freed object as a result of the call."; + OPCODE_BREAK; + } + if (obj->is_class_ptr(GDScriptFunctionState::get_class_ptr_static())) { + err_text = R"(Trying to call an async function without "await".)"; + OPCODE_BREAK; + } + } +#endif } else { base->call_ptr(*methodname, (const Variant **)argptrs, argc, nullptr, err); } @@ -1192,105 +1213,96 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } DISPATCH_OPCODE; - OPCODE(OPCODE_YIELD) - OPCODE(OPCODE_YIELD_SIGNAL) { - int ipofs = 1; - if (_code_ptr[ip] == OPCODE_YIELD_SIGNAL) { - CHECK_SPACE(4); - ipofs += 2; - } else { - CHECK_SPACE(2); - } + OPCODE(OPCODE_AWAIT) { + int ipofs = 2; + CHECK_SPACE(3); - Ref<GDScriptFunctionState> gdfs = memnew(GDScriptFunctionState); - gdfs->function = this; + //do the oneshot connect + GET_VARIANT_PTR(argobj, 1); + + Signal sig; + bool is_signal = true; - gdfs->state.stack.resize(alloca_size); - //copy variant stack - for (int i = 0; i < _stack_size; i++) { - memnew_placement(&gdfs->state.stack.write[sizeof(Variant) * i], Variant(stack[i])); - } - gdfs->state.stack_size = _stack_size; - gdfs->state.self = self; - gdfs->state.alloca_size = alloca_size; - gdfs->state.ip = ip + ipofs; - gdfs->state.line = line; - gdfs->state.script = _script; { - MutexLock lock(GDScriptLanguage::get_singleton()->lock); - _script->pending_func_states.add(&gdfs->scripts_list); - if (p_instance) { - gdfs->state.instance = p_instance; - p_instance->pending_func_states.add(&gdfs->instances_list); - } else { - gdfs->state.instance = nullptr; - } - } -#ifdef DEBUG_ENABLED - gdfs->state.function_name = name; - gdfs->state.script_path = _script->get_path(); -#endif - gdfs->state.defarg = defarg; - gdfs->function = this; + Variant result = *argobj; - retvalue = gdfs; + if (argobj->get_type() == Variant::OBJECT) { + bool was_freed = false; + Object *obj = argobj->get_validated_object_with_check(was_freed); - if (_code_ptr[ip] == OPCODE_YIELD_SIGNAL) { - //do the oneshot connect - GET_VARIANT_PTR(argobj, 1); - GET_VARIANT_PTR(argname, 2); + if (was_freed) { + err_text = "Trying to await on a freed object."; + OPCODE_BREAK; + } -#ifdef DEBUG_ENABLED - if (argobj->get_type() != Variant::OBJECT) { - err_text = "First argument of yield() not of type object."; - OPCODE_BREAK; - } - if (argname->get_type() != Variant::STRING) { - err_text = "Second argument of yield() not a string (for signal name)."; - OPCODE_BREAK; + // Is this even possible to be null at this point? + if (obj) { + if (obj->is_class_ptr(GDScriptFunctionState::get_class_ptr_static())) { + static StringName completed = _scs_create("completed"); + result = Signal(obj, completed); + } + } } -#endif -#ifdef DEBUG_ENABLED - bool was_freed; - Object *obj = argobj->get_validated_object_with_check(was_freed); - String signal = argname->operator String(); - - if (was_freed) { - err_text = "First argument of yield() is a previously freed instance."; - OPCODE_BREAK; + if (result.get_type() != Variant::SIGNAL) { + ip += 4; // Skip OPCODE_AWAIT_RESUME and its data. + // The stack pointer should be the same, so we don't need to set a return value. + is_signal = false; + } else { + sig = result; } + } - if (!obj) { - err_text = "First argument of yield() is null."; - OPCODE_BREAK; + if (is_signal) { + Ref<GDScriptFunctionState> gdfs = memnew(GDScriptFunctionState); + gdfs->function = this; + + gdfs->state.stack.resize(alloca_size); + //copy variant stack + for (int i = 0; i < _stack_size; i++) { + memnew_placement(&gdfs->state.stack.write[sizeof(Variant) * i], Variant(stack[i])); } - if (signal.length() == 0) { - err_text = "Second argument of yield() is an empty string (for signal name)."; - OPCODE_BREAK; + gdfs->state.stack_size = _stack_size; + gdfs->state.self = self; + gdfs->state.alloca_size = alloca_size; + gdfs->state.ip = ip + ipofs; + gdfs->state.line = line; + gdfs->state.script = _script; + { + MutexLock lock(GDScriptLanguage::get_singleton()->lock); + _script->pending_func_states.add(&gdfs->scripts_list); + if (p_instance) { + gdfs->state.instance = p_instance; + p_instance->pending_func_states.add(&gdfs->instances_list); + } else { + gdfs->state.instance = nullptr; + } } +#ifdef DEBUG_ENABLED + gdfs->state.function_name = name; + gdfs->state.script_path = _script->get_path(); +#endif + gdfs->state.defarg = defarg; + gdfs->function = this; - Error err = obj->connect_compat(signal, gdfs.ptr(), "_signal_callback", varray(gdfs), Object::CONNECT_ONESHOT); + retvalue = gdfs; + + Error err = sig.connect(Callable(gdfs.ptr(), "_signal_callback"), varray(gdfs), Object::CONNECT_ONESHOT); if (err != OK) { - err_text = "Error connecting to signal: " + signal + " during yield()."; + err_text = "Error connecting to signal: " + sig.get_name() + " during await."; OPCODE_BREAK; } -#else - Object *obj = argobj->operator Object *(); - String signal = argname->operator String(); - - obj->connect_compat(signal, gdfs.ptr(), "_signal_callback", varray(gdfs), Object::CONNECT_ONESHOT); -#endif - } #ifdef DEBUG_ENABLED - exit_ok = true; - yielded = true; + exit_ok = true; + awaited = true; #endif - OPCODE_BREAK; + OPCODE_BREAK; + } } + DISPATCH_OPCODE; // Needed for synchronous calls (when result is immediately available). - OPCODE(OPCODE_YIELD_RESUME) { + OPCODE(OPCODE_AWAIT_RESUME) { CHECK_SPACE(2); #ifdef DEBUG_ENABLED if (!p_state) { @@ -1556,11 +1568,11 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GDScriptLanguage::get_singleton()->script_frame_time += time_taken - function_call_time; } - // Check if this is the last time the function is resuming from yield - // Will be true if never yielded as well + // Check if this is the last time the function is resuming from await + // Will be true if never awaited as well // When it's the last resume it will postpone the exit from stack, // so the debugger knows which function triggered the resume of the next function (if any) - if (!p_state || yielded) { + if (!p_state || awaited) { if (EngineDebugger::is_active()) { GDScriptLanguage::get_singleton()->exit_function(); } @@ -1786,14 +1798,14 @@ Variant GDScriptFunctionState::resume(const Variant &p_arg) { if (!scripts_list.in_list()) { #ifdef DEBUG_ENABLED - ERR_FAIL_V_MSG(Variant(), "Resumed function '" + state.function_name + "()' after yield, but script is gone. At script: " + state.script_path + ":" + itos(state.line)); + ERR_FAIL_V_MSG(Variant(), "Resumed function '" + state.function_name + "()' after await, but script is gone. At script: " + state.script_path + ":" + itos(state.line)); #else return Variant(); #endif } if (state.instance && !instances_list.in_list()) { #ifdef DEBUG_ENABLED - ERR_FAIL_V_MSG(Variant(), "Resumed function '" + state.function_name + "()' after yield, but class instance is gone. At script: " + state.script_path + ":" + itos(state.line)); + ERR_FAIL_V_MSG(Variant(), "Resumed function '" + state.function_name + "()' after await, but class instance is gone. At script: " + state.script_path + ":" + itos(state.line)); #else return Variant(); #endif @@ -1810,7 +1822,7 @@ Variant GDScriptFunctionState::resume(const Variant &p_arg) { bool completed = true; // If the return value is a GDScriptFunctionState reference, - // then the function did yield again after resuming. + // then the function did awaited again after resuming. if (ret.is_ref()) { GDScriptFunctionState *gdfs = Object::cast_to<GDScriptFunctionState>(ret); if (gdfs && gdfs->function == function) { diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 583eab744a..771baf6a08 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -180,12 +180,12 @@ public: OPCODE_CONSTRUCT_DICTIONARY, OPCODE_CALL, OPCODE_CALL_RETURN, + OPCODE_CALL_ASYNC, OPCODE_CALL_BUILT_IN, OPCODE_CALL_SELF, OPCODE_CALL_SELF_BASE, - OPCODE_YIELD, - OPCODE_YIELD_SIGNAL, - OPCODE_YIELD_RESUME, + OPCODE_AWAIT, + OPCODE_AWAIT_RESUME, OPCODE_JUMP, OPCODE_JUMP_IF, OPCODE_JUMP_IF_NOT, diff --git a/modules/gdscript/gdscript_functions.cpp b/modules/gdscript/gdscript_functions.cpp index d4258c385e..fefbf906f0 100644 --- a/modules/gdscript/gdscript_functions.cpp +++ b/modules/gdscript/gdscript_functions.cpp @@ -146,12 +146,14 @@ void GDScriptFunctions::call(Function p_func, const Variant **p_args, int p_arg_ if (p_arg_count < m_count) { \ r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; \ r_error.argument = m_count; \ + r_error.expected = m_count; \ r_ret = Variant(); \ return; \ } \ if (p_arg_count > m_count) { \ r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; \ r_error.argument = m_count; \ + r_error.expected = m_count; \ r_ret = Variant(); \ return; \ } @@ -897,6 +899,7 @@ void GDScriptFunctions::call(Function p_func, const Variant **p_args, int p_arg_ case 0: { r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; r_error.argument = 1; + r_error.expected = 1; r_ret = Variant(); } break; @@ -1001,6 +1004,7 @@ void GDScriptFunctions::call(Function p_func, const Variant **p_args, int p_arg_ default: { r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS; r_error.argument = 3; + r_error.expected = 3; r_ret = Variant(); } break; @@ -1632,7 +1636,7 @@ MethodInfo GDScriptFunctions::get_info(Function p_func) { return mi; } break; case MATH_SMOOTHSTEP: { - MethodInfo mi("smoothstep", PropertyInfo(Variant::FLOAT, "from"), PropertyInfo(Variant::FLOAT, "to"), PropertyInfo(Variant::FLOAT, "weight")); + MethodInfo mi("smoothstep", PropertyInfo(Variant::FLOAT, "from"), PropertyInfo(Variant::FLOAT, "to"), PropertyInfo(Variant::FLOAT, "s")); mi.return_val.type = Variant::FLOAT; return mi; } break; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 63da849723..aeec1c0379 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -30,8870 +30,3812 @@ #include "gdscript_parser.h" -#include "core/core_string_names.h" -#include "core/engine.h" #include "core/io/resource_loader.h" +#include "core/math/math_defs.h" #include "core/os/file_access.h" -#include "core/print_string.h" #include "core/project_settings.h" -#include "core/reference.h" -#include "core/script_language.h" #include "gdscript.h" -template <class T> -T *GDScriptParser::alloc_node() { - T *t = memnew(T); - - t->next = list; - list = t; +#ifdef DEBUG_ENABLED +#include "core/os/os.h" +#include "core/string_builder.h" +#endif // DEBUG_ENABLED - if (!head) { - head = t; +#ifdef TOOLS_ENABLED +#include "editor/editor_settings.h" +#endif // TOOLS_ENABLED + +static HashMap<StringName, Variant::Type> builtin_types; +Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { + if (builtin_types.empty()) { + builtin_types["bool"] = Variant::BOOL; + builtin_types["int"] = Variant::INT; + builtin_types["float"] = Variant::FLOAT; + builtin_types["String"] = Variant::STRING; + builtin_types["Vector2"] = Variant::VECTOR2; + builtin_types["Vector2i"] = Variant::VECTOR2I; + builtin_types["Rect2"] = Variant::RECT2; + builtin_types["Rect2i"] = Variant::RECT2I; + builtin_types["Transform2D"] = Variant::TRANSFORM2D; + builtin_types["Vector3"] = Variant::VECTOR3; + builtin_types["Vector3i"] = Variant::VECTOR3I; + builtin_types["AABB"] = Variant::AABB; + builtin_types["Plane"] = Variant::PLANE; + builtin_types["Quat"] = Variant::QUAT; + builtin_types["Basis"] = Variant::BASIS; + builtin_types["Transform"] = Variant::TRANSFORM; + builtin_types["Color"] = Variant::COLOR; + builtin_types["RID"] = Variant::_RID; + builtin_types["Object"] = Variant::OBJECT; + builtin_types["StringName"] = Variant::STRING_NAME; + builtin_types["NodePath"] = Variant::NODE_PATH; + builtin_types["Dictionary"] = Variant::DICTIONARY; + builtin_types["Callable"] = Variant::CALLABLE; + builtin_types["Signal"] = Variant::SIGNAL; + builtin_types["Array"] = Variant::ARRAY; + builtin_types["PackedByteArray"] = Variant::PACKED_BYTE_ARRAY; + builtin_types["PackedInt32Array"] = Variant::PACKED_INT32_ARRAY; + builtin_types["PackedInt64Array"] = Variant::PACKED_INT64_ARRAY; + builtin_types["PackedFloat32Array"] = Variant::PACKED_FLOAT32_ARRAY; + builtin_types["PackedFloat64Array"] = Variant::PACKED_FLOAT64_ARRAY; + builtin_types["PackedStringArray"] = Variant::PACKED_STRING_ARRAY; + builtin_types["PackedVector2Array"] = Variant::PACKED_VECTOR2_ARRAY; + builtin_types["PackedVector3Array"] = Variant::PACKED_VECTOR3_ARRAY; + builtin_types["PackedColorArray"] = Variant::PACKED_COLOR_ARRAY; + // NIL is not here, hence the -1. + if (builtin_types.size() != Variant::VARIANT_MAX - 1) { + ERR_PRINT("Outdated parser: amount of built-in types don't match the amount of types in Variant."); + } + } + + if (builtin_types.has(p_type)) { + return builtin_types[p_type]; + } + return Variant::VARIANT_MAX; +} + +void GDScriptParser::cleanup() { + builtin_types.clear(); +} + +GDScriptFunctions::Function GDScriptParser::get_builtin_function(const StringName &p_name) { + for (int i = 0; i < GDScriptFunctions::FUNC_MAX; i++) { + if (p_name == GDScriptFunctions::get_func_name(GDScriptFunctions::Function(i))) { + return GDScriptFunctions::Function(i); + } + } + return GDScriptFunctions::FUNC_MAX; +} + +void GDScriptParser::get_annotation_list(List<MethodInfo> *r_annotations) const { + List<StringName> keys; + valid_annotations.get_key_list(&keys); + for (const List<StringName>::Element *E = keys.front(); E != nullptr; E = E->next()) { + r_annotations->push_back(valid_annotations[E->get()].info); } +} - t->line = tokenizer->get_token_line(); - t->column = tokenizer->get_token_column(); - return t; +GDScriptParser::GDScriptParser() { + // Register valid annotations. + // TODO: Should this be static? + // TODO: Validate applicable types (e.g. a VARIABLE annotation that only applies to string variables). + register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); + register_annotation(MethodInfo("@icon", { Variant::STRING, "icon_path" }), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation); + register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); + // Export annotations. + register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_TYPE_STRING, Variant::NIL>); + register_annotation(MethodInfo("@export_enum", { Variant::STRING, "names" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_ENUM, Variant::INT>, 0, true); + register_annotation(MethodInfo("@export_file", { Variant::STRING, "filter" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FILE, Variant::STRING>, 1, true); + register_annotation(MethodInfo("@export_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_DIR, Variant::STRING>); + register_annotation(MethodInfo("@export_global_file", { Variant::STRING, "filter" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_FILE, Variant::STRING>, 1, true); + register_annotation(MethodInfo("@export_global_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_DIR, Variant::STRING>); + register_annotation(MethodInfo("@export_multiline"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_MULTILINE_TEXT, Variant::STRING>); + register_annotation(MethodInfo("@export_placeholder"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_PLACEHOLDER_TEXT, Variant::STRING>); + register_annotation(MethodInfo("@export_range", { Variant::FLOAT, "min" }, { Variant::FLOAT, "max" }, { Variant::FLOAT, "step" }, { Variant::STRING, "slider1" }, { Variant::STRING, "slider2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_RANGE, Variant::FLOAT>, 3); + register_annotation(MethodInfo("@export_exp_range", { Variant::FLOAT, "min" }, { Variant::FLOAT, "max" }, { Variant::FLOAT, "step" }, { Variant::STRING, "slider1" }, { Variant::STRING, "slider2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_EXP_RANGE, Variant::FLOAT>, 3); + register_annotation(MethodInfo("@export_exp_easing", { Variant::STRING, "hint1" }, { Variant::STRING, "hint2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_EXP_EASING, Variant::FLOAT>, 2); + register_annotation(MethodInfo("@export_color_no_alpha"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_COLOR_NO_ALPHA, Variant::COLOR>); + register_annotation(MethodInfo("@export_node_path", { Variant::STRING, "type" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NODE_PATH_VALID_TYPES, Variant::NODE_PATH>, 1, true); + register_annotation(MethodInfo("@export_flags", { Variant::STRING, "names" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FLAGS, Variant::INT>, 0, true); + register_annotation(MethodInfo("@export_flags_2d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_RENDER, Variant::INT>); + register_annotation(MethodInfo("@export_flags_2d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_PHYSICS, Variant::INT>); + register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>); + register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>); + // Networking. + register_annotation(MethodInfo("@remote"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_REMOTE>); + register_annotation(MethodInfo("@master"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_MASTER>); + register_annotation(MethodInfo("@puppet"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_PUPPET>); + register_annotation(MethodInfo("@remotesync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_REMOTESYNC>); + register_annotation(MethodInfo("@mastersync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_MASTERSYNC>); + register_annotation(MethodInfo("@puppetsync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations<MultiplayerAPI::RPC_MODE_PUPPETSYNC>); + // TODO: Warning annotations. } -#ifdef DEBUG_ENABLED -static String _find_function_name(const GDScriptParser::OperatorNode *p_call); -#endif // DEBUG_ENABLED +GDScriptParser::~GDScriptParser() { + clear(); +} -bool GDScriptParser::_end_statement() { - if (tokenizer->get_token() == GDScriptTokenizer::TK_SEMICOLON) { - tokenizer->advance(); - return true; //handle next - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE || tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - return true; //will be handled properly +void GDScriptParser::clear() { + while (list != nullptr) { + Node *element = list; + list = list->next; + memdelete(element); } - return false; + head = nullptr; + list = nullptr; + _is_tool = false; + for_completion = false; + errors.clear(); + multiline_stack.clear(); } -void GDScriptParser::_set_end_statement_error(String p_name) { - String error_msg; - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER) { - error_msg = vformat("Expected end of statement (\"%s\"), got %s (\"%s\") instead.", p_name, tokenizer->get_token_name(tokenizer->get_token()), tokenizer->get_token_identifier()); +void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { + // TODO: Improve error reporting by pointing at source code. + // TODO: Errors might point at more than one place at once (e.g. show previous declaration). + panic_mode = true; + // TODO: Improve positional information. + if (p_origin == nullptr) { + errors.push_back({ p_message, current.start_line, current.start_column }); } else { - error_msg = vformat("Expected end of statement (\"%s\"), got %s instead.", p_name, tokenizer->get_token_name(tokenizer->get_token())); + errors.push_back({ p_message, p_origin->start_line, p_origin->leftmost_column }); } - _set_error(error_msg); } -bool GDScriptParser::_enter_indent_block(BlockNode *p_block) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_COLON) { - // report location at the previous token (on the previous line) - int error_line = tokenizer->get_token_line(-1); - int error_column = tokenizer->get_token_column(-1); - _set_error("':' expected at end of line.", error_line, error_column); - return false; +#ifdef DEBUG_ENABLED +void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_code, const String &p_symbol1, const String &p_symbol2, const String &p_symbol3, const String &p_symbol4) { + Vector<String> symbols; + if (!p_symbol1.empty()) { + symbols.push_back(p_symbol1); } - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - return false; + if (!p_symbol2.empty()) { + symbols.push_back(p_symbol2); } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_NEWLINE) { - // be more python-like - IndentLevel current_level = indent_level.back()->get(); - indent_level.push_back(current_level); - return true; - //_set_error("newline expected after ':'."); - //return false; - } - - while (true) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_NEWLINE) { - return false; //wtf - } else if (tokenizer->get_token(1) == GDScriptTokenizer::TK_EOF) { - return false; - } else if (tokenizer->get_token(1) != GDScriptTokenizer::TK_NEWLINE) { - int indent = tokenizer->get_token_line_indent(); - int tabs = tokenizer->get_token_line_tab_indent(); - IndentLevel current_level = indent_level.back()->get(); - IndentLevel new_indent(indent, tabs); - if (new_indent.is_mixed(current_level)) { - _set_error("Mixed tabs and spaces in indentation."); - return false; - } - - if (indent <= current_level.indent) { - return false; - } - - indent_level.push_back(new_indent); - tokenizer->advance(); - return true; - - } else if (p_block) { - NewLineNode *nl = alloc_node<NewLineNode>(); - nl->line = tokenizer->get_token_line(); - p_block->statements.push_back(nl); - } - - tokenizer->advance(); // go to next newline + if (!p_symbol3.empty()) { + symbols.push_back(p_symbol3); } + if (!p_symbol4.empty()) { + symbols.push_back(p_symbol4); + } + push_warning(p_source, p_code, symbols); } -bool GDScriptParser::_parse_arguments(Node *p_parent, Vector<Node *> &p_args, bool p_static, bool p_can_codecomplete, bool p_parsing_constant) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - tokenizer->advance(); - } else { - parenthesis++; - int argidx = 0; - - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - _make_completable_call(argidx); - completion_node = p_parent; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING && tokenizer->get_token(1) == GDScriptTokenizer::TK_CURSOR) { - //completing a string argument.. - completion_cursor = tokenizer->get_token_constant(); - - _make_completable_call(argidx); - completion_node = p_parent; - tokenizer->advance(1); - return false; - } - - Node *arg = _parse_expression(p_parent, p_static, false, p_parsing_constant); - if (!arg) { - return false; - } - - p_args.push_back(arg); +void GDScriptParser::push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector<String> &p_symbols) { + if (is_ignoring_warnings) { + return; + } + if (GLOBAL_GET("debug/gdscript/warnings/exclude_addons").booleanize() && script_path.begins_with("res://addons/")) { + return; + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - tokenizer->advance(); - break; + String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower(); + if (ignored_warnings.has(warn_name)) { + return; + } + if (!GLOBAL_GET("debug/gdscript/warnings/" + warn_name)) { + return; + } - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expression expected"); - return false; - } + GDScriptWarning warning; + warning.code = p_code; + warning.symbols = p_symbols; + warning.start_line = p_source->start_line; + warning.end_line = p_source->end_line; + warning.leftmost_column = p_source->leftmost_column; + warning.rightmost_column = p_source->rightmost_column; - tokenizer->advance(); - argidx++; - } else { - // something is broken - _set_error("Expected ',' or ')'"); - return false; - } + List<GDScriptWarning>::Element *before = nullptr; + for (List<GDScriptWarning>::Element *E = warnings.front(); E != nullptr; E = E->next()) { + if (E->get().start_line > warning.start_line) { + break; } - parenthesis--; + before = E; + } + if (before) { + warnings.insert_after(before, warning); + } else { + warnings.push_front(warning); } - - return true; } +#endif -void GDScriptParser::_make_completable_call(int p_arg) { - completion_cursor = StringName(); - completion_type = COMPLETION_CALL_ARGUMENTS; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = p_arg; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); -} - -bool GDScriptParser::_get_completable_identifier(CompletionType p_type, StringName &identifier) { - identifier = StringName(); - if (tokenizer->is_token_literal()) { - identifier = tokenizer->get_token_literal(); - tokenizer->advance(); - } - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = identifier; - completion_type = p_type; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_block = current_block; - completion_found = true; - completion_ident_is_call = false; - tokenizer->advance(); - - if (tokenizer->is_token_literal()) { - identifier = identifier.operator String() + tokenizer->get_token_literal().operator String(); - tokenizer->advance(); - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - completion_ident_is_call = true; - } - return true; +void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node, int p_argument, bool p_force) { + if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { + return; } - - return false; + if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + return; + } + CompletionContext context; + context.type = p_type; + context.current_class = current_class; + context.current_function = current_function; + context.current_suite = current_suite; + context.current_line = tokenizer.get_cursor_line(); + context.current_argument = p_argument; + context.node = p_node; + completion_context = context; } -GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_static, bool p_allow_assign, bool p_parsing_constant) { - //Vector<Node*> expressions; - //Vector<OperatorNode::Operator> operators; - - Vector<Expression> expression; - - Node *expr = nullptr; - - int op_line = tokenizer->get_token_line(); // when operators are created at the bottom, the line might have been changed (\n found) - - while (true) { - /*****************/ - /* Parse Operand */ - /*****************/ - - if (parenthesis > 0) { - //remove empty space (only allowed if inside parenthesis - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } - } - - // Check that the next token is not TK_CURSOR and if it is, the offset should be incremented. - int next_valid_offset = 1; - if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_CURSOR) { - next_valid_offset++; - // There is a chunk of the identifier that also needs to be ignored (not always there!) - if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_IDENTIFIER) { - next_valid_offset++; - } - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - //subexpression () - tokenizer->advance(); - parenthesis++; - Node *subexpr = _parse_expression(p_parent, p_static, p_allow_assign, p_parsing_constant); - parenthesis--; - if (!subexpr) { - return nullptr; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected ')' in expression"); - return nullptr; - } - - tokenizer->advance(); - expr = subexpr; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_DOLLAR) { - tokenizer->advance(); - - String path; - - bool need_identifier = true; - bool done = false; - int line = tokenizer->get_token_line(); - - while (!done) { - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_CURSOR: { - completion_type = COMPLETION_GET_NODE; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_cursor = path; - completion_argument = 0; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); - } break; - case GDScriptTokenizer::TK_CONSTANT: { - if (!need_identifier) { - done = true; - break; - } - - if (tokenizer->get_token_constant().get_type() != Variant::STRING) { - _set_error("Expected string constant or identifier after '$' or '/'."); - return nullptr; - } - - path += String(tokenizer->get_token_constant()); - tokenizer->advance(); - need_identifier = false; - - } break; - case GDScriptTokenizer::TK_OP_DIV: { - if (need_identifier) { - done = true; - break; - } - - path += "/"; - tokenizer->advance(); - need_identifier = true; - - } break; - default: { - // Instead of checking for TK_IDENTIFIER, we check with is_token_literal, as this allows us to use match/sync/etc. as a name - if (need_identifier && tokenizer->is_token_literal()) { - path += String(tokenizer->get_token_literal()); - tokenizer->advance(); - need_identifier = false; - } else { - done = true; - } - - break; - } - } - } - - if (path == "") { - _set_error("Path expected after $."); - return nullptr; - } - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_CALL; - op->line = line; - op->arguments.push_back(alloc_node<SelfNode>()); - op->arguments[0]->line = line; - - IdentifierNode *funcname = alloc_node<IdentifierNode>(); - funcname->name = "get_node"; - funcname->line = line; - op->arguments.push_back(funcname); - - ConstantNode *nodepath = alloc_node<ConstantNode>(); - nodepath->value = NodePath(StringName(path)); - nodepath->datatype = _type_from_variant(nodepath->value); - nodepath->line = line; - op->arguments.push_back(nodepath); - - expr = op; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - tokenizer->advance(); - continue; //no point in cursor in the middle of expression - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = tokenizer->get_token_constant(); - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_PI) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = Math_PI; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_TAU) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = Math_TAU; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_INF) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = Math_INF; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_NAN) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = Math_NAN; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_PRELOAD) { - //constant defined by tokenizer - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected '(' after 'preload'"); - return nullptr; - } - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = StringName(); - completion_node = p_parent; - completion_type = COMPLETION_RESOURCE_PATH; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_block = current_block; - completion_argument = 0; - completion_found = true; - tokenizer->advance(); - } +void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force) { + if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { + return; + } + if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + return; + } + CompletionContext context; + context.type = p_type; + context.current_class = current_class; + context.current_function = current_function; + context.current_suite = current_suite; + context.current_line = tokenizer.get_cursor_line(); + context.builtin_type = p_builtin_type; + completion_context = context; +} - String path; - bool found_constant = false; - bool valid = false; - ConstantNode *cn; +void GDScriptParser::push_completion_call(Node *p_call) { + if (!for_completion) { + return; + } + CompletionCall call; + call.call = p_call; + call.argument = 0; + completion_call_stack.push_back(call); + if (previous.cursor_place == GDScriptTokenizer::CURSOR_MIDDLE || previous.cursor_place == GDScriptTokenizer::CURSOR_END || current.cursor_place == GDScriptTokenizer::CURSOR_BEGINNING) { + completion_call = call; + } +} - Node *subexpr = _parse_and_reduce_expression(p_parent, p_static); - if (subexpr) { - if (subexpr->type == Node::TYPE_CONSTANT) { - cn = static_cast<ConstantNode *>(subexpr); - found_constant = true; - } - if (subexpr->type == Node::TYPE_IDENTIFIER) { - IdentifierNode *in = static_cast<IdentifierNode *>(subexpr); - - // Try to find the constant expression by the identifier - if (current_class->constant_expressions.has(in->name)) { - Node *cn_exp = current_class->constant_expressions[in->name].expression; - if (cn_exp->type == Node::TYPE_CONSTANT) { - cn = static_cast<ConstantNode *>(cn_exp); - found_constant = true; - } - } - } +void GDScriptParser::pop_completion_call() { + if (!for_completion) { + return; + } + ERR_FAIL_COND_MSG(completion_call_stack.empty(), "Trying to pop empty completion call stack"); + completion_call_stack.pop_back(); +} - if (found_constant && cn->value.get_type() == Variant::STRING) { - valid = true; - path = (String)cn->value; - } - } +void GDScriptParser::set_last_completion_call_arg(int p_argument) { + if (!for_completion || passed_cursor) { + return; + } + ERR_FAIL_COND_MSG(completion_call_stack.empty(), "Trying to set argument on empty completion call stack"); + completion_call_stack.back()->get().argument = p_argument; +} - if (!valid) { - _set_error("expected string constant as 'preload' argument."); - return nullptr; - } +Error GDScriptParser::parse(const String &p_source_code, const String &p_script_path, bool p_for_completion) { + clear(); - if (!path.is_abs_path() && base_path != "") { - path = base_path.plus_file(path); - } - path = path.replace("///", "//").simplify_path(); - if (path == self_path) { - _set_error("Can't preload itself (use 'get_script()')."); - return nullptr; - } + String source = p_source_code; + int cursor_line = -1; + int cursor_column = -1; + for_completion = p_for_completion; - Ref<Resource> res; - dependencies.push_back(path); - if (!dependencies_only) { - if (!validating) { - //this can be too slow for just validating code - if (for_completion && ScriptCodeCompletionCache::get_singleton() && FileAccess::exists(path)) { - res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); - } else if (!for_completion || FileAccess::exists(path)) { - res = ResourceLoader::load(path); - } - } else { - if (!FileAccess::exists(path)) { - _set_error("Can't preload resource at path: " + path); - return nullptr; - } else if (ScriptCodeCompletionCache::get_singleton()) { - res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); - } - } - if (!res.is_valid()) { - _set_error("Can't preload resource at path: " + path); - return nullptr; + int tab_size = 4; +#ifdef TOOLS_ENABLED + if (EditorSettings::get_singleton()) { + tab_size = EditorSettings::get_singleton()->get_setting("text_editor/indent/size"); + } +#endif // TOOLS_ENABLED + + if (p_for_completion) { + // Remove cursor sentinel char. + const Vector<String> lines = p_source_code.split("\n"); + cursor_line = 1; + cursor_column = 1; + for (int i = 0; i < lines.size(); i++) { + bool found = false; + const String &line = lines[i]; + for (int j = 0; j < line.size(); j++) { + if (line[j] == CharType(0xFFFF)) { + found = true; + break; + } else if (line[j] == '\t') { + cursor_column += tab_size - 1; } + cursor_column++; } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected ')' after 'preload' path"); - return nullptr; - } - - Ref<GDScript> gds = res; - if (gds.is_valid() && !gds->is_valid()) { - _set_error("Couldn't fully preload the script, possible cyclic reference or compilation error. Use \"load()\" instead if a cyclic reference is intended."); - return nullptr; - } - - tokenizer->advance(); - - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = res; - constant->datatype = _type_from_variant(constant->value); - - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_YIELD) { - if (!current_function) { - _set_error("\"yield()\" can only be used inside function blocks."); - return nullptr; - } - - current_function->has_yield = true; - - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected \"(\" after \"yield\"."); - return nullptr; - } - - tokenizer->advance(); - - OperatorNode *yield = alloc_node<OperatorNode>(); - yield->op = OperatorNode::OP_YIELD; - - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); + if (found) { + break; } + cursor_line++; + cursor_column = 1; + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - expr = yield; - tokenizer->advance(); - } else { - parenthesis++; + source = source.replace_first(String::chr(0xFFFF), String()); + } - Node *object = _parse_and_reduce_expression(p_parent, p_static); - if (!object) { - return nullptr; - } - yield->arguments.push_back(object); + tokenizer.set_source_code(source); + tokenizer.set_cursor_position(cursor_line, cursor_column); + script_path = p_script_path; + current = tokenizer.scan(); + // Avoid error as the first token. + while (current.type == GDScriptTokenizer::Token::ERROR) { + push_error(current.literal); + current = tokenizer.scan(); + } - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - _set_error("Expected \",\" after the first argument of \"yield\"."); - return nullptr; - } + push_multiline(false); // Keep one for the whole parsing. + parse_program(); + pop_multiline(); - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = StringName(); - completion_node = object; - completion_type = COMPLETION_YIELD; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = 0; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); - } +#ifdef DEBUG_ENABLED + if (multiline_stack.size() > 0) { + ERR_PRINT("Parser bug: Imbalanced multiline stack."); + } +#endif - Node *signal = _parse_and_reduce_expression(p_parent, p_static); - if (!signal) { - return nullptr; - } - yield->arguments.push_back(signal); + if (errors.empty()) { + return OK; + } else { + return ERR_PARSE_ERROR; + } +} - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" after the second argument of \"yield\"."); - return nullptr; - } +GDScriptTokenizer::Token GDScriptParser::advance() { + if (current.type == GDScriptTokenizer::Token::TK_EOF) { + ERR_FAIL_COND_V_MSG(current.type == GDScriptTokenizer::Token::TK_EOF, current, "GDScript parser bug: Trying to advance past the end of stream."); + } + if (for_completion && !completion_call_stack.empty()) { + if (completion_call.call == nullptr && tokenizer.is_past_cursor()) { + completion_call = completion_call_stack.back()->get(); + passed_cursor = true; + } + } + previous = current; + current = tokenizer.scan(); + while (current.type == GDScriptTokenizer::Token::ERROR) { + push_error(current.literal); + current = tokenizer.scan(); + } + return previous; +} - parenthesis--; +bool GDScriptParser::match(GDScriptTokenizer::Token::Type p_token_type) { + if (!check(p_token_type)) { + return false; + } + advance(); + return true; +} - tokenizer->advance(); +bool GDScriptParser::check(GDScriptTokenizer::Token::Type p_token_type) { + if (p_token_type == GDScriptTokenizer::Token::IDENTIFIER) { + return current.is_identifier(); + } + return current.type == p_token_type; +} - expr = yield; - } +bool GDScriptParser::consume(GDScriptTokenizer::Token::Type p_token_type, const String &p_error_message) { + if (match(p_token_type)) { + return true; + } + push_error(p_error_message); + return false; +} - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_SELF) { - if (p_static) { - _set_error("\"self\" isn't allowed in a static function or constant expression."); - return nullptr; - } - //constant defined by tokenizer - SelfNode *self = alloc_node<SelfNode>(); - tokenizer->advance(); - expr = self; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE && tokenizer->get_token(1) == GDScriptTokenizer::TK_PERIOD) { - Variant::Type bi_type = tokenizer->get_token_type(); - tokenizer->advance(2); - - StringName identifier; - - if (_get_completable_identifier(COMPLETION_BUILT_IN_TYPE_CONSTANT, identifier)) { - completion_built_in_constant = bi_type; - } +bool GDScriptParser::is_at_end() { + return check(GDScriptTokenizer::Token::TK_EOF); +} - if (identifier == StringName()) { - _set_error("Built-in type constant or static function expected after \".\"."); - return nullptr; - } - if (!Variant::has_constant(bi_type, identifier)) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN && - Variant::is_method_const(bi_type, identifier) && - Variant::get_method_return_type(bi_type, identifier) == bi_type) { - tokenizer->advance(); +void GDScriptParser::synchronize() { + panic_mode = false; + while (!is_at_end()) { + if (previous.type == GDScriptTokenizer::Token::NEWLINE || previous.type == GDScriptTokenizer::Token::SEMICOLON) { + return; + } - OperatorNode *construct = alloc_node<OperatorNode>(); - construct->op = OperatorNode::OP_CALL; + switch (current.type) { + case GDScriptTokenizer::Token::CLASS: + case GDScriptTokenizer::Token::FUNC: + case GDScriptTokenizer::Token::STATIC: + case GDScriptTokenizer::Token::VAR: + case GDScriptTokenizer::Token::CONST: + case GDScriptTokenizer::Token::SIGNAL: + //case GDScriptTokenizer::Token::IF: // Can also be inside expressions. + case GDScriptTokenizer::Token::FOR: + case GDScriptTokenizer::Token::WHILE: + case GDScriptTokenizer::Token::MATCH: + case GDScriptTokenizer::Token::RETURN: + case GDScriptTokenizer::Token::ANNOTATION: + return; + default: + // Do nothing. + break; + } - TypeNode *tn = alloc_node<TypeNode>(); - tn->vtype = bi_type; - construct->arguments.push_back(tn); + advance(); + } +} - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_CALL; - op->arguments.push_back(construct); +void GDScriptParser::push_multiline(bool p_state) { + multiline_stack.push_back(p_state); + tokenizer.set_multiline_mode(p_state); + if (p_state) { + // Consume potential whitespace tokens already waiting in line. + while (current.type == GDScriptTokenizer::Token::NEWLINE || current.type == GDScriptTokenizer::Token::INDENT || current.type == GDScriptTokenizer::Token::DEDENT) { + current = tokenizer.scan(); // Don't call advance() here, as we don't want to change the previous token. + } + } +} - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; - op->arguments.push_back(id); +void GDScriptParser::pop_multiline() { + ERR_FAIL_COND_MSG(multiline_stack.size() == 0, "Parser bug: trying to pop from multiline stack without available value."); + multiline_stack.pop_back(); + tokenizer.set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); +} - if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { - return nullptr; - } +bool GDScriptParser::is_statement_end() { + return check(GDScriptTokenizer::Token::NEWLINE) || check(GDScriptTokenizer::Token::SEMICOLON) || check(GDScriptTokenizer::Token::TK_EOF); +} - expr = op; - } else { - // Object is a special case - bool valid = false; - if (bi_type == Variant::OBJECT) { - int object_constant = ClassDB::get_integer_constant("Object", identifier, &valid); - if (valid) { - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = object_constant; - cn->datatype = _type_from_variant(cn->value); - expr = cn; - } - } - if (!valid) { - _set_error("Static constant '" + identifier.operator String() + "' not present in built-in type " + Variant::get_type_name(bi_type) + "."); - return nullptr; - } - } - } else { - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = Variant::get_constant_value(bi_type, identifier); - cn->datatype = _type_from_variant(cn->value); - expr = cn; - } +void GDScriptParser::end_statement(const String &p_context) { + bool found = false; + while (is_statement_end() && !is_at_end()) { + // Remove sequential newlines/semicolons. + found = true; + advance(); + } + if (!found && !is_at_end()) { + push_error(vformat(R"(Expected end of statement after %s, found "%s" instead.)", p_context, current.get_name())); + } +} - } else if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_PARENTHESIS_OPEN && tokenizer->is_token_literal()) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - //function or constructor - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_CALL; - - //Do a quick Array and Dictionary Check. Replace if either require no arguments. - bool replaced = false; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE) { - Variant::Type ct = tokenizer->get_token_type(); - if (!p_parsing_constant) { - if (ct == Variant::ARRAY) { - if (tokenizer->get_token(2) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - ArrayNode *arr = alloc_node<ArrayNode>(); - expr = arr; - replaced = true; - tokenizer->advance(3); - } - } - if (ct == Variant::DICTIONARY) { - if (tokenizer->get_token(2) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - DictionaryNode *dict = alloc_node<DictionaryNode>(); - expr = dict; - replaced = true; - tokenizer->advance(3); - } - } - } +void GDScriptParser::parse_program() { + head = alloc_node<ClassNode>(); + current_class = head; - if (!replaced) { - TypeNode *tn = alloc_node<TypeNode>(); - tn->vtype = tokenizer->get_token_type(); - op->arguments.push_back(tn); - tokenizer->advance(2); + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + // Check for @tool annotation. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + if (annotation->name == "@tool") { + // TODO: don't allow @tool anywhere else. (Should all script annotations be the first thing?). + _is_tool = true; + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after "@tool" annotation.)"); } - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_FUNC) { - BuiltInFunctionNode *bn = alloc_node<BuiltInFunctionNode>(); - bn->function = tokenizer->get_token_built_in_func(); - op->arguments.push_back(bn); - tokenizer->advance(2); + // @tool annotation has no specific target. + annotation->apply(this, nullptr); } else { - SelfNode *self = alloc_node<SelfNode>(); - op->arguments.push_back(self); - - StringName identifier; - if (_get_completable_identifier(COMPLETION_FUNCTION, identifier)) { - } - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; - op->arguments.push_back(id); - tokenizer->advance(1); + annotation_stack.push_back(annotation); } + } + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - _make_completable_call(0); - completion_node = op; - - if (op->arguments[0]->type == GDScriptParser::Node::Type::TYPE_BUILT_IN_FUNCTION) { - BuiltInFunctionNode *bn = static_cast<BuiltInFunctionNode *>(op->arguments[0]); - if (bn->function == GDScriptFunctions::Function::RESOURCE_LOAD) { - completion_type = COMPLETION_RESOURCE_PATH; - } - } - } - if (!replaced) { - if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { - return nullptr; - } - expr = op; - } - } else if (tokenizer->is_token_literal(0, true)) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - //identifier (reference) - - const ClassNode *cln = current_class; - bool bfn = false; - StringName identifier; - int id_line = tokenizer->get_token_line(); - if (_get_completable_identifier(COMPLETION_IDENTIFIER, identifier)) { - } - - BlockNode *b = current_block; - while (!bfn && b) { - if (b->variables.has(identifier)) { - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; - id->declared_block = b; - id->line = id_line; - expr = id; - bfn = true; - -#ifdef DEBUG_ENABLED - LocalVarNode *lv = b->variables[identifier]; - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_OP_ASSIGN_ADD: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_OR: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_XOR: - case GDScriptTokenizer::TK_OP_ASSIGN_DIV: - case GDScriptTokenizer::TK_OP_ASSIGN_MOD: - case GDScriptTokenizer::TK_OP_ASSIGN_MUL: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT: - case GDScriptTokenizer::TK_OP_ASSIGN_SUB: { - if (lv->assignments == 0) { - if (!lv->datatype.has_type) { - _set_error("Using assignment with operation on a variable that was never assigned."); - return nullptr; - } - _add_warning(GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, -1, identifier.operator String()); - } - [[fallthrough]]; - } - case GDScriptTokenizer::TK_OP_ASSIGN: { - lv->assignments += 1; - lv->usages--; // Assignment is not really usage - } break; - default: { - lv->usages++; - } - } -#endif // DEBUG_ENABLED - break; - } - b = b->parent_block; - } - - if (!bfn && p_parsing_constant) { - if (cln->constant_expressions.has(identifier)) { - expr = cln->constant_expressions[identifier].expression; - bfn = true; - } else if (GDScriptLanguage::get_singleton()->get_global_map().has(identifier)) { - //check from constants - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = GDScriptLanguage::get_singleton()->get_global_array()[GDScriptLanguage::get_singleton()->get_global_map()[identifier]]; - constant->datatype = _type_from_variant(constant->value); - constant->line = id_line; - expr = constant; - bfn = true; - } - - if (!bfn && GDScriptLanguage::get_singleton()->get_named_globals_map().has(identifier)) { - //check from singletons - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = GDScriptLanguage::get_singleton()->get_named_globals_map()[identifier]; - constant->datatype = _type_from_variant(constant->value); - expr = constant; - bfn = true; - } - - if (!dependencies_only) { - if (!bfn && ScriptServer::is_global_class(identifier)) { - Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(identifier)); - if (scr.is_valid() && scr->is_valid()) { - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = scr; - constant->datatype = _type_from_variant(constant->value); - expr = constant; - bfn = true; - } - } - - // Check parents for the constant - if (!bfn) { - // Using current_class instead of cln here, since cln is const* - _determine_inheritance(current_class, false); - if (cln->base_type.has_type && cln->base_type.kind == DataType::GDSCRIPT && cln->base_type.script_type->is_valid()) { - Map<StringName, Variant> parent_constants; - current_class->base_type.script_type->get_constants(&parent_constants); - if (parent_constants.has(identifier)) { - ConstantNode *constant = alloc_node<ConstantNode>(); - constant->value = parent_constants[identifier]; - constant->datatype = _type_from_variant(constant->value); - expr = constant; - bfn = true; - } - } - } - } - } - - if (!bfn) { -#ifdef DEBUG_ENABLED - if (current_function) { - int arg_idx = current_function->arguments.find(identifier); - if (arg_idx != -1) { - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_OP_ASSIGN_ADD: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_OR: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_XOR: - case GDScriptTokenizer::TK_OP_ASSIGN_DIV: - case GDScriptTokenizer::TK_OP_ASSIGN_MOD: - case GDScriptTokenizer::TK_OP_ASSIGN_MUL: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT: - case GDScriptTokenizer::TK_OP_ASSIGN_SUB: - case GDScriptTokenizer::TK_OP_ASSIGN: { - // Assignment is not really usage - } break; - default: { - current_function->arguments_usage.write[arg_idx] = current_function->arguments_usage[arg_idx] + 1; - } - } - } - } -#endif // DEBUG_ENABLED - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; - id->line = id_line; - expr = id; - } - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_ADD || tokenizer->get_token() == GDScriptTokenizer::TK_OP_SUB || tokenizer->get_token() == GDScriptTokenizer::TK_OP_NOT || tokenizer->get_token() == GDScriptTokenizer::TK_OP_BIT_INVERT) { - //single prefix operators like !expr +expr -expr ++expr --expr - alloc_node<OperatorNode>(); - Expression e; - e.is_op = true; - - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_OP_ADD: - e.op = OperatorNode::OP_POS; - break; - case GDScriptTokenizer::TK_OP_SUB: - e.op = OperatorNode::OP_NEG; - break; - case GDScriptTokenizer::TK_OP_NOT: - e.op = OperatorNode::OP_NOT; - break; - case GDScriptTokenizer::TK_OP_BIT_INVERT: - e.op = OperatorNode::OP_BIT_INVERT; - break; - default: { + for (bool should_break = false; !should_break;) { + // Order here doesn't matter, but there should be only one of each at most. + switch (current.type) { + case GDScriptTokenizer::Token::CLASS_NAME: + if (!annotation_stack.empty()) { + push_error(R"("class_name" should be used before annotations.)"); } - } - - tokenizer->advance(); - - if (e.op != OperatorNode::OP_NOT && tokenizer->get_token() == GDScriptTokenizer::TK_OP_NOT) { - _set_error("Misplaced 'not'."); - return nullptr; - } - - expression.push_back(e); - continue; //only exception, must continue... - - /* - Node *subexpr=_parse_expression(op,p_static); - if (!subexpr) - return nullptr; - op->arguments.push_back(subexpr); - expr=op;*/ - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_IS && tokenizer->get_token(1) == GDScriptTokenizer::TK_BUILT_IN_TYPE) { - // 'is' operator with built-in type - if (!expr) { - _set_error("Expected identifier before 'is' operator"); - return nullptr; - } - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_IS_BUILTIN; - op->arguments.push_back(expr); - - tokenizer->advance(); - - TypeNode *tn = alloc_node<TypeNode>(); - tn->vtype = tokenizer->get_token_type(); - op->arguments.push_back(tn); - tokenizer->advance(); - - expr = op; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_OPEN) { - // array - tokenizer->advance(); - - ArrayNode *arr = alloc_node<ArrayNode>(); - bool expecting_comma = false; - - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - _set_error("Unterminated array"); - return nullptr; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_CLOSE) { - tokenizer->advance(); - break; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); //ignore newline - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - if (!expecting_comma) { - _set_error("expression or ']' expected"); - return nullptr; - } - - expecting_comma = false; - tokenizer->advance(); //ignore newline + advance(); + if (head->identifier != nullptr) { + push_error(R"("class_name" can only be used once.)"); } else { - //parse expression - if (expecting_comma) { - _set_error("',' or ']' expected"); - return nullptr; - } - Node *n = _parse_expression(arr, p_static, p_allow_assign, p_parsing_constant); - if (!n) { - return nullptr; - } - arr->elements.push_back(n); - expecting_comma = true; + parse_class_name(); } - } - - expr = arr; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_OPEN) { - // array - tokenizer->advance(); - - DictionaryNode *dict = alloc_node<DictionaryNode>(); - - enum DictExpect { - - DICT_EXPECT_KEY, - DICT_EXPECT_COLON, - DICT_EXPECT_VALUE, - DICT_EXPECT_COMMA - - }; - - Node *key = nullptr; - Set<Variant> keys; - - DictExpect expecting = DICT_EXPECT_KEY; - - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - _set_error("Unterminated dictionary"); - return nullptr; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - if (expecting == DICT_EXPECT_COLON) { - _set_error("':' expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_VALUE) { - _set_error("value expected"); - return nullptr; - } - tokenizer->advance(); - break; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); //ignore newline - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - if (expecting == DICT_EXPECT_KEY) { - _set_error("key or '}' expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_VALUE) { - _set_error("value expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_COLON) { - _set_error("':' expected"); - return nullptr; - } - - expecting = DICT_EXPECT_KEY; - tokenizer->advance(); //ignore newline - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - if (expecting == DICT_EXPECT_KEY) { - _set_error("key or '}' expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_VALUE) { - _set_error("value expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_COMMA) { - _set_error("',' or '}' expected"); - return nullptr; - } - - expecting = DICT_EXPECT_VALUE; - tokenizer->advance(); //ignore newline + break; + case GDScriptTokenizer::Token::EXTENDS: + if (!annotation_stack.empty()) { + push_error(R"("extends" should be used before annotations.)"); + } + advance(); + if (head->extends_used) { + push_error(R"("extends" can only be used once.)"); } else { - if (expecting == DICT_EXPECT_COMMA) { - _set_error("',' or '}' expected"); - return nullptr; - } - if (expecting == DICT_EXPECT_COLON) { - _set_error("':' expected"); - return nullptr; - } - - if (expecting == DICT_EXPECT_KEY) { - if (tokenizer->is_token_literal() && tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - //lua style identifier, easier to write - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = tokenizer->get_token_literal(); - cn->datatype = _type_from_variant(cn->value); - key = cn; - tokenizer->advance(2); - expecting = DICT_EXPECT_VALUE; - } else { - //python/js style more flexible - key = _parse_expression(dict, p_static, p_allow_assign, p_parsing_constant); - if (!key) { - return nullptr; - } - expecting = DICT_EXPECT_COLON; - } - } - - if (expecting == DICT_EXPECT_VALUE) { - Node *value = _parse_expression(dict, p_static, p_allow_assign, p_parsing_constant); - if (!value) { - return nullptr; - } - expecting = DICT_EXPECT_COMMA; - - if (key->type == GDScriptParser::Node::TYPE_CONSTANT) { - Variant const &keyName = static_cast<const GDScriptParser::ConstantNode *>(key)->value; - - if (keys.has(keyName)) { - _set_error("Duplicate key found in Dictionary literal"); - return nullptr; - } - keys.insert(keyName); - } - - DictionaryNode::Pair pair; - pair.key = key; - pair.value = value; - dict->elements.push_back(pair); - key = nullptr; - } + parse_extends(); + end_statement("superclass"); } - } - - expr = dict; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD && (tokenizer->is_token_literal(1) || tokenizer->get_token(1) == GDScriptTokenizer::TK_CURSOR)) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - // parent call - - tokenizer->advance(); //goto identifier - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_PARENT_CALL; - - /*SelfNode *self = alloc_node<SelfNode>(); - op->arguments.push_back(self); - forbidden for now */ - StringName identifier; - bool is_completion = _get_completable_identifier(COMPLETION_PARENT_FUNCTION, identifier) && for_completion; + break; + default: + should_break = true; + break; + } - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; - op->arguments.push_back(id); + if (panic_mode) { + synchronize(); + } + } - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - if (!is_completion) { - _set_error("Expected '(' for parent function call."); - return nullptr; + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + // Check for @icon annotation. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + if (annotation->name == "@icon") { + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after "@icon" annotation.)"); } + annotation->apply(this, head); } else { - tokenizer->advance(); - if (!_parse_arguments(op, op->arguments, p_static, false, p_parsing_constant)) { - return nullptr; - } + annotation_stack.push_back(annotation); } - - expr = op; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE && expression.size() > 0 && expression[expression.size() - 1].is_op && expression[expression.size() - 1].op == OperatorNode::OP_IS) { - Expression e = expression[expression.size() - 1]; - e.op = OperatorNode::OP_IS_BUILTIN; - expression.write[expression.size() - 1] = e; - - TypeNode *tn = alloc_node<TypeNode>(); - tn->vtype = tokenizer->get_token_type(); - expr = tn; - tokenizer->advance(); - } else { - //find list [ or find dictionary { - _set_error("Error parsing expression, misplaced: " + String(tokenizer->get_token_name(tokenizer->get_token()))); - return nullptr; //nothing } + } - ERR_FAIL_COND_V_MSG(!expr, nullptr, "GDScriptParser bug, couldn't figure out what expression is."); - - /******************/ - /* Parse Indexing */ - /******************/ - - while (true) { - //expressions can be indexed any number of times - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD) { - //indexing using "." - - if (tokenizer->get_token(1) != GDScriptTokenizer::TK_CURSOR && !tokenizer->is_token_literal(1)) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - _set_error("Expected identifier as member"); - return nullptr; - } else if (tokenizer->get_token(2) == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - //call!! - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_CALL; - - tokenizer->advance(); - - IdentifierNode *id = alloc_node<IdentifierNode>(); - StringName identifier; - if (_get_completable_identifier(COMPLETION_METHOD, identifier)) { - completion_node = op; - //indexing stuff - } - - id->name = identifier; - - op->arguments.push_back(expr); // call what - op->arguments.push_back(id); // call func - //get arguments - tokenizer->advance(1); - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - _make_completable_call(0); - completion_node = op; - } - if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { - return nullptr; - } - expr = op; + parse_class_body(); - } else { - //simple indexing! + if (!check(GDScriptTokenizer::Token::TK_EOF)) { + push_error("Expected end of file."); + } - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_INDEX_NAMED; - tokenizer->advance(); + clear_unused_annotations(); +} - StringName identifier; - if (_get_completable_identifier(COMPLETION_INDEX, identifier)) { - if (identifier == StringName()) { - identifier = "@temp"; //so it parses alright - } - completion_node = op; +GDScriptParser::ClassNode *GDScriptParser::parse_class() { + ClassNode *n_class = alloc_node<ClassNode>(); - //indexing stuff - } + ClassNode *previous_class = current_class; + current_class = n_class; + n_class->outer = previous_class; - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = identifier; + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) { + n_class->identifier = parse_identifier(); + } - op->arguments.push_back(expr); - op->arguments.push_back(id); + if (match(GDScriptTokenizer::Token::EXTENDS)) { + parse_extends(); + } - expr = op; - } + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after class declaration.)"); + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected newline after class declaration.)"); - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_OPEN) { - //indexing using "[]" - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_INDEX; + if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected indented block after class declaration.)")) { + current_class = previous_class; + return n_class; + } - tokenizer->advance(1); + parse_class_body(); - Node *subexpr = _parse_expression(op, p_static, p_allow_assign, p_parsing_constant); - if (!subexpr) { - return nullptr; - } + consume(GDScriptTokenizer::Token::DEDENT, R"(Missing unindent at the end of the class body.)"); - if (tokenizer->get_token() != GDScriptTokenizer::TK_BRACKET_CLOSE) { - _set_error("Expected ']'"); - return nullptr; - } + current_class = previous_class; + return n_class; +} - op->arguments.push_back(expr); - op->arguments.push_back(subexpr); - tokenizer->advance(1); - expr = op; +void GDScriptParser::parse_class_name() { + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the global class name after "class_name".)")) { + current_class->identifier = parse_identifier(); + } - } else { - break; + // TODO: Move this to annotation + if (match(GDScriptTokenizer::Token::COMMA)) { + // Icon path. + if (consume(GDScriptTokenizer::Token::LITERAL, R"(Expected class icon path string after ",".)")) { + if (previous.literal.get_type() != Variant::STRING) { + push_error(vformat(R"(Only strings can be used for the class icon path, found "%s" instead.)", Variant::get_type_name(previous.literal.get_type()))); } + current_class->icon_path = previous.literal; } + } - /*****************/ - /* Parse Casting */ - /*****************/ + if (match(GDScriptTokenizer::Token::EXTENDS)) { + // Allow extends on the same line. + parse_extends(); + end_statement("superclass"); + } else { + end_statement("class_name statement"); + } +} - bool has_casting = expr->type == Node::TYPE_CAST; - if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_AS) { - if (has_casting) { - _set_error("Unexpected 'as'."); - return nullptr; - } - CastNode *cn = alloc_node<CastNode>(); - if (!_parse_type(cn->cast_type)) { - _set_error("Expected type after 'as'."); - return nullptr; - } - has_casting = true; - cn->source_node = expr; - expr = cn; - } +void GDScriptParser::parse_extends() { + current_class->extends_used = true; - /******************/ - /* Parse Operator */ - /******************/ + int chain_index = 0; - if (parenthesis > 0) { - //remove empty space (only allowed if inside parenthesis - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } + if (match(GDScriptTokenizer::Token::LITERAL)) { + if (previous.literal.get_type() != Variant::STRING) { + push_error(vformat(R"(Only strings or identifiers can be used after "extends", found "%s" instead.)", Variant::get_type_name(previous.literal.get_type()))); } + current_class->extends_path = previous.literal; - Expression e; - e.is_op = false; - e.node = expr; - expression.push_back(e); + if (!match(GDScriptTokenizer::Token::PERIOD)) { + end_statement("superclass path"); + return; + } + } - // determine which operator is next + make_completion_context(COMPLETION_INHERIT_TYPE, current_class, chain_index++); - OperatorNode::Operator op; - bool valid = true; + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after "extends".)")) { + return; + } + current_class->extends.push_back(previous.literal); -//assign, if allowed is only allowed on the first operator -#define _VALIDATE_ASSIGN \ - if (!p_allow_assign || has_casting) { \ - _set_error("Unexpected assign."); \ - return nullptr; \ - } \ - p_allow_assign = false; + while (match(GDScriptTokenizer::Token::PERIOD)) { + make_completion_context(COMPLETION_INHERIT_TYPE, current_class, chain_index++); + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after ".".)")) { + return; + } + current_class->extends.push_back(previous.literal); + } +} - switch (tokenizer->get_token()) { //see operator +template <class T> +void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind) { + advance(); + T *member = (this->*p_parse_function)(); + if (member == nullptr) { + return; + } + // Consume annotations. + while (!annotation_stack.empty()) { + AnnotationNode *last_annotation = annotation_stack.back()->get(); + if (last_annotation->applies_to(p_target)) { + last_annotation->apply(this, member); + member->annotations.push_front(last_annotation); + annotation_stack.pop_back(); + } else { + push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind)); + clear_unused_annotations(); + return; + } + } + if (member->identifier != nullptr) { + // Enums may be unnamed. + // TODO: Consider names in outer scope too, for constants and classes (and static functions?) + if (current_class->members_indices.has(member->identifier->name)) { + push_error(vformat(R"(%s "%s" has the same name as a previously declared %s.)", p_member_kind.capitalize(), member->identifier->name, current_class->get_member(member->identifier->name).get_type_name()), member->identifier); + } else { + current_class->add_member(member); + } + } +} - case GDScriptTokenizer::TK_OP_IN: - op = OperatorNode::OP_IN; - break; - case GDScriptTokenizer::TK_OP_EQUAL: - op = OperatorNode::OP_EQUAL; - break; - case GDScriptTokenizer::TK_OP_NOT_EQUAL: - op = OperatorNode::OP_NOT_EQUAL; - break; - case GDScriptTokenizer::TK_OP_LESS: - op = OperatorNode::OP_LESS; - break; - case GDScriptTokenizer::TK_OP_LESS_EQUAL: - op = OperatorNode::OP_LESS_EQUAL; - break; - case GDScriptTokenizer::TK_OP_GREATER: - op = OperatorNode::OP_GREATER; - break; - case GDScriptTokenizer::TK_OP_GREATER_EQUAL: - op = OperatorNode::OP_GREATER_EQUAL; +void GDScriptParser::parse_class_body() { + bool class_end = false; + while (!class_end && !is_at_end()) { + switch (current.type) { + case GDScriptTokenizer::Token::VAR: + parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable"); break; - case GDScriptTokenizer::TK_OP_AND: - op = OperatorNode::OP_AND; + case GDScriptTokenizer::Token::CONST: + parse_class_member(&GDScriptParser::parse_constant, AnnotationInfo::CONSTANT, "constant"); break; - case GDScriptTokenizer::TK_OP_OR: - op = OperatorNode::OP_OR; + case GDScriptTokenizer::Token::SIGNAL: + parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal"); break; - case GDScriptTokenizer::TK_OP_ADD: - op = OperatorNode::OP_ADD; + case GDScriptTokenizer::Token::STATIC: + case GDScriptTokenizer::Token::FUNC: + parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function"); break; - case GDScriptTokenizer::TK_OP_SUB: - op = OperatorNode::OP_SUB; + case GDScriptTokenizer::Token::CLASS: + parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class"); break; - case GDScriptTokenizer::TK_OP_MUL: - op = OperatorNode::OP_MUL; + case GDScriptTokenizer::Token::ENUM: + parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum"); break; - case GDScriptTokenizer::TK_OP_DIV: - op = OperatorNode::OP_DIV; - break; - case GDScriptTokenizer::TK_OP_MOD: - op = OperatorNode::OP_MOD; - break; - //case GDScriptTokenizer::TK_OP_NEG: op=OperatorNode::OP_NEG ; break; - case GDScriptTokenizer::TK_OP_SHIFT_LEFT: - op = OperatorNode::OP_SHIFT_LEFT; - break; - case GDScriptTokenizer::TK_OP_SHIFT_RIGHT: - op = OperatorNode::OP_SHIFT_RIGHT; - break; - case GDScriptTokenizer::TK_OP_ASSIGN: { - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN; - - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_CURSOR) { - //code complete assignment - completion_type = COMPLETION_ASSIGN; - completion_node = expr; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_block = current_block; - completion_found = true; - tokenizer->advance(); + case GDScriptTokenizer::Token::ANNOTATION: { + advance(); + AnnotationNode *annotation = parse_annotation(AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + annotation_stack.push_back(annotation); } - - } break; - case GDScriptTokenizer::TK_OP_ASSIGN_ADD: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_ADD; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_SUB: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_SUB; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_MUL: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_MUL; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_DIV: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_DIV; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_MOD: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_MOD; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_SHIFT_LEFT; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_SHIFT_RIGHT; break; - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_BIT_AND; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_OR: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_BIT_OR; - break; - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_XOR: - _VALIDATE_ASSIGN op = OperatorNode::OP_ASSIGN_BIT_XOR; - break; - case GDScriptTokenizer::TK_OP_BIT_AND: - op = OperatorNode::OP_BIT_AND; - break; - case GDScriptTokenizer::TK_OP_BIT_OR: - op = OperatorNode::OP_BIT_OR; - break; - case GDScriptTokenizer::TK_OP_BIT_XOR: - op = OperatorNode::OP_BIT_XOR; - break; - case GDScriptTokenizer::TK_PR_IS: - op = OperatorNode::OP_IS; - break; - case GDScriptTokenizer::TK_CF_IF: - op = OperatorNode::OP_TERNARY_IF; + } + case GDScriptTokenizer::Token::PASS: + advance(); + end_statement(R"("pass")"); break; - case GDScriptTokenizer::TK_CF_ELSE: - op = OperatorNode::OP_TERNARY_ELSE; + case GDScriptTokenizer::Token::DEDENT: + class_end = true; break; default: - valid = false; + push_error(vformat(R"(Unexpected "%s" in class body.)", current.get_name())); + advance(); break; } - - if (valid) { - e.is_op = true; - e.op = op; - expression.push_back(e); - tokenizer->advance(); - } else { - break; + if (panic_mode) { + synchronize(); } } +} - /* Reduce the set set of expressions and place them in an operator tree, respecting precedence */ - - while (expression.size() > 1) { - int next_op = -1; - int min_priority = 0xFFFFF; - bool is_unary = false; - bool is_ternary = false; - - for (int i = 0; i < expression.size(); i++) { - if (!expression[i].is_op) { - continue; - } - - int priority; - - bool unary = false; - bool ternary = false; - bool error = false; - bool right_to_left = false; - - switch (expression[i].op) { - case OperatorNode::OP_IS: - case OperatorNode::OP_IS_BUILTIN: - priority = -1; - break; //before anything - - case OperatorNode::OP_BIT_INVERT: - priority = 0; - unary = true; - break; - case OperatorNode::OP_NEG: - case OperatorNode::OP_POS: - priority = 1; - unary = true; - break; - - case OperatorNode::OP_MUL: - priority = 2; - break; - case OperatorNode::OP_DIV: - priority = 2; - break; - case OperatorNode::OP_MOD: - priority = 2; - break; - - case OperatorNode::OP_ADD: - priority = 3; - break; - case OperatorNode::OP_SUB: - priority = 3; - break; - - case OperatorNode::OP_SHIFT_LEFT: - priority = 4; - break; - case OperatorNode::OP_SHIFT_RIGHT: - priority = 4; - break; - - case OperatorNode::OP_BIT_AND: - priority = 5; - break; - case OperatorNode::OP_BIT_XOR: - priority = 6; - break; - case OperatorNode::OP_BIT_OR: - priority = 7; - break; - - case OperatorNode::OP_LESS: - priority = 8; - break; - case OperatorNode::OP_LESS_EQUAL: - priority = 8; - break; - case OperatorNode::OP_GREATER: - priority = 8; - break; - case OperatorNode::OP_GREATER_EQUAL: - priority = 8; - break; - - case OperatorNode::OP_EQUAL: - priority = 8; - break; - case OperatorNode::OP_NOT_EQUAL: - priority = 8; - break; - - case OperatorNode::OP_IN: - priority = 10; - break; +GDScriptParser::VariableNode *GDScriptParser::parse_variable() { + return parse_variable(true); +} - case OperatorNode::OP_NOT: - priority = 11; - unary = true; - break; - case OperatorNode::OP_AND: - priority = 12; - break; - case OperatorNode::OP_OR: - priority = 13; - break; +GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_property) { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) { + return nullptr; + } - case OperatorNode::OP_TERNARY_IF: - priority = 14; - ternary = true; - right_to_left = true; - break; - case OperatorNode::OP_TERNARY_ELSE: - priority = 14; - error = true; - // Rigth-to-left should be false in this case, otherwise it would always error. - break; + VariableNode *variable = alloc_node<VariableNode>(); + variable->identifier = parse_identifier(); - case OperatorNode::OP_ASSIGN: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_ADD: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_SUB: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_MUL: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_DIV: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_MOD: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_SHIFT_LEFT: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_BIT_AND: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_BIT_OR: - priority = 15; - break; - case OperatorNode::OP_ASSIGN_BIT_XOR: - priority = 15; - break; + if (match(GDScriptTokenizer::Token::COLON)) { + if (check(GDScriptTokenizer::Token::NEWLINE)) { + if (p_allow_property) { + advance(); - default: { - _set_error("GDScriptParser bug, invalid operator in expression: " + itos(expression[i].op)); - return nullptr; - } + return parse_property(variable, true); + } else { + push_error(R"(Expected type after ":")"); + return nullptr; } - - if (priority < min_priority || (right_to_left && priority == min_priority)) { - // < is used for left to right (default) - // <= is used for right to left - if (error) { - _set_error("Unexpected operator"); - return nullptr; + } else if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + variable->infer_datatype = true; + } else { + if (p_allow_property) { + make_completion_context(COMPLETION_PROPERTY_DECLARATION_OR_TYPE, variable); + if (check(GDScriptTokenizer::Token::IDENTIFIER)) { + // Check if get or set. + if (current.get_identifier() == "get" || current.get_identifier() == "set") { + return parse_property(variable, false); + } } - next_op = i; - min_priority = priority; - is_unary = unary; - is_ternary = ternary; } - } - if (next_op == -1) { - _set_error("Yet another parser bug...."); - ERR_FAIL_V(nullptr); + // Parse type. + variable->datatype_specifier = parse_type(); } + } - // OK! create operator.. - if (is_unary) { - int expr_pos = next_op; - while (expression[expr_pos].is_op) { - expr_pos++; - if (expr_pos == expression.size()) { - //can happen.. - _set_error("Unexpected end of expression..."); - return nullptr; - } - } - - //consecutively do unary operators - for (int i = expr_pos - 1; i >= next_op; i--) { - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = expression[i].op; - op->arguments.push_back(expression[i + 1].node); - op->line = op_line; //line might have been changed from a \n - expression.write[i].is_op = false; - expression.write[i].node = op; - expression.remove(i + 1); - } - - } else if (is_ternary) { - if (next_op < 1 || next_op >= (expression.size() - 1)) { - _set_error("Parser bug..."); - ERR_FAIL_V(nullptr); - } - - if (next_op >= (expression.size() - 2) || expression[next_op + 2].op != OperatorNode::OP_TERNARY_ELSE) { - _set_error("Expected else after ternary if."); - return nullptr; - } - if (next_op >= (expression.size() - 3)) { - _set_error("Expected value after ternary else."); - return nullptr; - } - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = expression[next_op].op; - op->line = op_line; //line might have been changed from a \n - - if (expression[next_op - 1].is_op) { - _set_error("Parser bug..."); - ERR_FAIL_V(nullptr); - } - - if (expression[next_op + 1].is_op) { - // this is not invalid and can really appear - // but it becomes invalid anyway because no binary op - // can be followed by a unary op in a valid combination, - // due to how precedence works, unaries will always disappear first - - _set_error("Unexpected two consecutive operators after ternary if."); - return nullptr; - } - - if (expression[next_op + 3].is_op) { - // this is not invalid and can really appear - // but it becomes invalid anyway because no binary op - // can be followed by a unary op in a valid combination, - // due to how precedence works, unaries will always disappear first - - _set_error("Unexpected two consecutive operators after ternary else."); - return nullptr; - } - - op->arguments.push_back(expression[next_op + 1].node); //next expression goes as first - op->arguments.push_back(expression[next_op - 1].node); //left expression goes as when-true - op->arguments.push_back(expression[next_op + 3].node); //expression after next goes as when-false + if (match(GDScriptTokenizer::Token::EQUAL)) { + // Initializer. + variable->initializer = parse_expression(false); + variable->assignments++; + } - //replace all 3 nodes by this operator and make it an expression - expression.write[next_op - 1].node = op; - expression.remove(next_op); - expression.remove(next_op); - expression.remove(next_op); - expression.remove(next_op); + if (p_allow_property && match(GDScriptTokenizer::Token::COLON)) { + if (match(GDScriptTokenizer::Token::NEWLINE)) { + return parse_property(variable, true); } else { - if (next_op < 1 || next_op >= (expression.size() - 1)) { - _set_error("Parser bug..."); - ERR_FAIL_V(nullptr); - } - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = expression[next_op].op; - op->line = op_line; //line might have been changed from a \n - - if (expression[next_op - 1].is_op) { - _set_error("Parser bug..."); - ERR_FAIL_V(nullptr); - } - - if (expression[next_op + 1].is_op) { - // this is not invalid and can really appear - // but it becomes invalid anyway because no binary op - // can be followed by a unary op in a valid combination, - // due to how precedence works, unaries will always disappear first - - _set_error("Unexpected two consecutive operators."); - return nullptr; - } - - op->arguments.push_back(expression[next_op - 1].node); //expression goes as left - op->arguments.push_back(expression[next_op + 1].node); //next expression goes as right - - //replace all 3 nodes by this operator and make it an expression - expression.write[next_op - 1].node = op; - expression.remove(next_op); - expression.remove(next_op); + return parse_property(variable, false); } } - return expression[0].node; -} - -GDScriptParser::Node *GDScriptParser::_reduce_expression(Node *p_node, bool p_to_const) { - switch (p_node->type) { - case Node::TYPE_BUILT_IN_FUNCTION: { - //many may probably be optimizable - return p_node; - } break; - case Node::TYPE_ARRAY: { - ArrayNode *an = static_cast<ArrayNode *>(p_node); - bool all_constants = true; - - for (int i = 0; i < an->elements.size(); i++) { - an->elements.write[i] = _reduce_expression(an->elements[i], p_to_const); - if (an->elements[i]->type != Node::TYPE_CONSTANT) { - all_constants = false; - } - } + end_statement("variable declaration"); - if (all_constants && p_to_const) { - //reduce constant array expression + variable->export_info.name = variable->identifier->name; - ConstantNode *cn = alloc_node<ConstantNode>(); - Array arr; - arr.resize(an->elements.size()); - for (int i = 0; i < an->elements.size(); i++) { - ConstantNode *acn = static_cast<ConstantNode *>(an->elements[i]); - arr[i] = acn->value; - } - cn->value = arr; - cn->datatype = _type_from_variant(cn->value); - return cn; - } - - return an; - - } break; - case Node::TYPE_DICTIONARY: { - DictionaryNode *dn = static_cast<DictionaryNode *>(p_node); - bool all_constants = true; - - for (int i = 0; i < dn->elements.size(); i++) { - dn->elements.write[i].key = _reduce_expression(dn->elements[i].key, p_to_const); - if (dn->elements[i].key->type != Node::TYPE_CONSTANT) { - all_constants = false; - } - dn->elements.write[i].value = _reduce_expression(dn->elements[i].value, p_to_const); - if (dn->elements[i].value->type != Node::TYPE_CONSTANT) { - all_constants = false; - } - } + return variable; +} - if (all_constants && p_to_const) { - //reduce constant array expression +GDScriptParser::VariableNode *GDScriptParser::parse_property(VariableNode *p_variable, bool p_need_indent) { + if (p_need_indent) { + if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected indented block for property after ":".)")) { + return nullptr; + } + } - ConstantNode *cn = alloc_node<ConstantNode>(); - Dictionary dict; - for (int i = 0; i < dn->elements.size(); i++) { - ConstantNode *key_c = static_cast<ConstantNode *>(dn->elements[i].key); - ConstantNode *value_c = static_cast<ConstantNode *>(dn->elements[i].value); + VariableNode *property = p_variable; - dict[key_c->value] = value_c->value; - } - cn->value = dict; - cn->datatype = _type_from_variant(cn->value); - return cn; - } + make_completion_context(COMPLETION_PROPERTY_DECLARATION, property); - return dn; + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected "get" or "set" for property declaration.)")) { + return nullptr; + } - } break; - case Node::TYPE_OPERATOR: { - OperatorNode *op = static_cast<OperatorNode *>(p_node); + IdentifierNode *function = parse_identifier(); - bool all_constants = true; - int last_not_constant = -1; + if (check(GDScriptTokenizer::Token::EQUAL)) { + p_variable->property = VariableNode::PROP_SETGET; + } else { + p_variable->property = VariableNode::PROP_INLINE; + if (!p_need_indent) { + push_error("Property with inline code must go to an indented block."); + } + } - for (int i = 0; i < op->arguments.size(); i++) { - op->arguments.write[i] = _reduce_expression(op->arguments[i], p_to_const); - if (op->arguments[i]->type != Node::TYPE_CONSTANT) { - all_constants = false; - last_not_constant = i; - } - } + bool getter_used = false; + bool setter_used = false; - if (op->op == OperatorNode::OP_IS) { - //nothing much - return op; + // Run with a loop because order doesn't matter. + for (int i = 0; i < 2; i++) { + if (function->name == "set") { + if (setter_used) { + push_error(R"(Properties can only have one setter.)"); + } else { + parse_property_setter(property); + setter_used = true; } - if (op->op == OperatorNode::OP_PARENT_CALL) { - //nothing much - return op; - - } else if (op->op == OperatorNode::OP_CALL) { - //can reduce base type constructors - if ((op->arguments[0]->type == Node::TYPE_TYPE || (op->arguments[0]->type == Node::TYPE_BUILT_IN_FUNCTION && GDScriptFunctions::is_deterministic(static_cast<BuiltInFunctionNode *>(op->arguments[0])->function))) && last_not_constant == 0) { - //native type constructor or intrinsic function - const Variant **vptr = nullptr; - Vector<Variant *> ptrs; - if (op->arguments.size() > 1) { - ptrs.resize(op->arguments.size() - 1); - for (int i = 0; i < ptrs.size(); i++) { - ConstantNode *cn = static_cast<ConstantNode *>(op->arguments[i + 1]); - ptrs.write[i] = &cn->value; - } - - vptr = (const Variant **)&ptrs[0]; - } - - Callable::CallError ce; - Variant v; - - if (op->arguments[0]->type == Node::TYPE_TYPE) { - TypeNode *tn = static_cast<TypeNode *>(op->arguments[0]); - v = Variant::construct(tn->vtype, vptr, ptrs.size(), ce); - - } else { - GDScriptFunctions::Function func = static_cast<BuiltInFunctionNode *>(op->arguments[0])->function; - GDScriptFunctions::call(func, vptr, ptrs.size(), v, ce); - } - - if (ce.error != Callable::CallError::CALL_OK) { - String errwhere; - if (op->arguments[0]->type == Node::TYPE_TYPE) { - TypeNode *tn = static_cast<TypeNode *>(op->arguments[0]); - errwhere = "'" + Variant::get_type_name(tn->vtype) + "' constructor"; - - } else { - GDScriptFunctions::Function func = static_cast<BuiltInFunctionNode *>(op->arguments[0])->function; - errwhere = String("'") + GDScriptFunctions::get_func_name(func) + "' intrinsic function"; - } - - switch (ce.error) { - case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: { - _set_error("Invalid argument (#" + itos(ce.argument + 1) + ") for " + errwhere + "."); - - } break; - case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS: { - _set_error("Too many arguments for " + errwhere + "."); - } break; - case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS: { - _set_error("Too few arguments for " + errwhere + "."); - } break; - default: { - _set_error("Invalid arguments for " + errwhere + "."); - - } break; - } - - error_line = op->line; - - return p_node; - } - - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = v; - cn->datatype = _type_from_variant(v); - return cn; - } - - return op; //don't reduce yet - - } else if (op->op == OperatorNode::OP_YIELD) { - return op; - - } else if (op->op == OperatorNode::OP_INDEX) { - //can reduce indices into constant arrays or dictionaries - - if (all_constants) { - ConstantNode *ca = static_cast<ConstantNode *>(op->arguments[0]); - ConstantNode *cb = static_cast<ConstantNode *>(op->arguments[1]); - - bool valid; - - Variant v = ca->value.get(cb->value, &valid); - if (!valid) { - _set_error("invalid index in constant expression"); - error_line = op->line; - return op; - } - - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = v; - cn->datatype = _type_from_variant(v); - return cn; - } - - return op; - - } else if (op->op == OperatorNode::OP_INDEX_NAMED) { - if (op->arguments[0]->type == Node::TYPE_CONSTANT && op->arguments[1]->type == Node::TYPE_IDENTIFIER) { - ConstantNode *ca = static_cast<ConstantNode *>(op->arguments[0]); - IdentifierNode *ib = static_cast<IdentifierNode *>(op->arguments[1]); - - bool valid; - Variant v = ca->value.get_named(ib->name, &valid); - if (!valid) { - _set_error("invalid index '" + String(ib->name) + "' in constant expression"); - error_line = op->line; - return op; - } - - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = v; - cn->datatype = _type_from_variant(v); - return cn; - } - - return op; + } else if (function->name == "get") { + if (getter_used) { + push_error(R"(Properties can only have one getter.)"); + } else { + parse_property_getter(property); + getter_used = true; } + } else { + // TODO: Update message to only have the missing one if it's the case. + push_error(R"(Expected "get" or "set" for property declaration.)"); + } - //validate assignment (don't assign to constant expression - switch (op->op) { - case OperatorNode::OP_ASSIGN: - case OperatorNode::OP_ASSIGN_ADD: - case OperatorNode::OP_ASSIGN_SUB: - case OperatorNode::OP_ASSIGN_MUL: - case OperatorNode::OP_ASSIGN_DIV: - case OperatorNode::OP_ASSIGN_MOD: - case OperatorNode::OP_ASSIGN_SHIFT_LEFT: - case OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - case OperatorNode::OP_ASSIGN_BIT_AND: - case OperatorNode::OP_ASSIGN_BIT_OR: - case OperatorNode::OP_ASSIGN_BIT_XOR: { - if (op->arguments[0]->type == Node::TYPE_CONSTANT) { - _set_error("Can't assign to constant", tokenizer->get_token_line() - 1); - error_line = op->line; - return op; - } else if (op->arguments[0]->type == Node::TYPE_SELF) { - _set_error("Can't assign to self.", op->line); - error_line = op->line; - return op; - } - - if (op->arguments[0]->type == Node::TYPE_OPERATOR) { - OperatorNode *on = static_cast<OperatorNode *>(op->arguments[0]); - if (on->op != OperatorNode::OP_INDEX && on->op != OperatorNode::OP_INDEX_NAMED) { - _set_error("Can't assign to an expression", tokenizer->get_token_line() - 1); - error_line = op->line; - return op; - } + if (i == 0 && p_variable->property == VariableNode::PROP_SETGET) { + if (match(GDScriptTokenizer::Token::COMMA)) { + // Consume potential newline. + if (match(GDScriptTokenizer::Token::NEWLINE)) { + if (!p_need_indent) { + push_error(R"(Inline setter/getter setting cannot span across multiple lines (use "\\"" if needed).)"); } - - } break; - default: { - break; } - } - //now se if all are constants - if (!all_constants) { - return op; //nothing to reduce from here on - } -#define _REDUCE_UNARY(m_vop) \ - bool valid = false; \ - Variant res; \ - Variant::evaluate(m_vop, static_cast<ConstantNode *>(op->arguments[0])->value, Variant(), res, valid); \ - if (!valid) { \ - _set_error("Invalid operand for unary operator"); \ - error_line = op->line; \ - return p_node; \ - } \ - ConstantNode *cn = alloc_node<ConstantNode>(); \ - cn->value = res; \ - cn->datatype = _type_from_variant(res); \ - return cn; - -#define _REDUCE_BINARY(m_vop) \ - bool valid = false; \ - Variant res; \ - Variant::evaluate(m_vop, static_cast<ConstantNode *>(op->arguments[0])->value, static_cast<ConstantNode *>(op->arguments[1])->value, res, valid); \ - if (!valid) { \ - _set_error("Invalid operands for operator"); \ - error_line = op->line; \ - return p_node; \ - } \ - ConstantNode *cn = alloc_node<ConstantNode>(); \ - cn->value = res; \ - cn->datatype = _type_from_variant(res); \ - return cn; - - switch (op->op) { - //unary operators - case OperatorNode::OP_NEG: { - _REDUCE_UNARY(Variant::OP_NEGATE); - } break; - case OperatorNode::OP_POS: { - _REDUCE_UNARY(Variant::OP_POSITIVE); - } break; - case OperatorNode::OP_NOT: { - _REDUCE_UNARY(Variant::OP_NOT); - } break; - case OperatorNode::OP_BIT_INVERT: { - _REDUCE_UNARY(Variant::OP_BIT_NEGATE); - } break; - //binary operators (in precedence order) - case OperatorNode::OP_IN: { - _REDUCE_BINARY(Variant::OP_IN); - } break; - case OperatorNode::OP_EQUAL: { - _REDUCE_BINARY(Variant::OP_EQUAL); - } break; - case OperatorNode::OP_NOT_EQUAL: { - _REDUCE_BINARY(Variant::OP_NOT_EQUAL); - } break; - case OperatorNode::OP_LESS: { - _REDUCE_BINARY(Variant::OP_LESS); - } break; - case OperatorNode::OP_LESS_EQUAL: { - _REDUCE_BINARY(Variant::OP_LESS_EQUAL); - } break; - case OperatorNode::OP_GREATER: { - _REDUCE_BINARY(Variant::OP_GREATER); - } break; - case OperatorNode::OP_GREATER_EQUAL: { - _REDUCE_BINARY(Variant::OP_GREATER_EQUAL); - } break; - case OperatorNode::OP_AND: { - _REDUCE_BINARY(Variant::OP_AND); - } break; - case OperatorNode::OP_OR: { - _REDUCE_BINARY(Variant::OP_OR); - } break; - case OperatorNode::OP_ADD: { - _REDUCE_BINARY(Variant::OP_ADD); - } break; - case OperatorNode::OP_SUB: { - _REDUCE_BINARY(Variant::OP_SUBTRACT); - } break; - case OperatorNode::OP_MUL: { - _REDUCE_BINARY(Variant::OP_MULTIPLY); - } break; - case OperatorNode::OP_DIV: { - _REDUCE_BINARY(Variant::OP_DIVIDE); - } break; - case OperatorNode::OP_MOD: { - _REDUCE_BINARY(Variant::OP_MODULE); - } break; - case OperatorNode::OP_SHIFT_LEFT: { - _REDUCE_BINARY(Variant::OP_SHIFT_LEFT); - } break; - case OperatorNode::OP_SHIFT_RIGHT: { - _REDUCE_BINARY(Variant::OP_SHIFT_RIGHT); - } break; - case OperatorNode::OP_BIT_AND: { - _REDUCE_BINARY(Variant::OP_BIT_AND); - } break; - case OperatorNode::OP_BIT_OR: { - _REDUCE_BINARY(Variant::OP_BIT_OR); - } break; - case OperatorNode::OP_BIT_XOR: { - _REDUCE_BINARY(Variant::OP_BIT_XOR); - } break; - case OperatorNode::OP_TERNARY_IF: { - if (static_cast<ConstantNode *>(op->arguments[0])->value.booleanize()) { - return op->arguments[1]; - } else { - return op->arguments[2]; - } - } break; - default: { - ERR_FAIL_V(op); - } - } - - } break; - default: { - return p_node; - } break; - } -} - -GDScriptParser::Node *GDScriptParser::_parse_and_reduce_expression(Node *p_parent, bool p_static, bool p_reduce_const, bool p_allow_assign) { - Node *expr = _parse_expression(p_parent, p_static, p_allow_assign, p_reduce_const); - if (!expr || error_set) { - return nullptr; - } - expr = _reduce_expression(expr, p_reduce_const); - if (!expr || error_set) { - return nullptr; - } - return expr; -} - -bool GDScriptParser::_reduce_export_var_type(Variant &p_value, int p_line) { - if (p_value.get_type() == Variant::ARRAY) { - Array arr = p_value; - for (int i = 0; i < arr.size(); i++) { - if (!_reduce_export_var_type(arr[i], p_line)) { - return false; + } else { + break; } } - return true; - } - if (p_value.get_type() == Variant::DICTIONARY) { - Dictionary dict = p_value; - for (int i = 0; i < dict.size(); i++) { - Variant value = dict.get_value_at_index(i); - if (!_reduce_export_var_type(value, p_line)) { - return false; - } + if (!match(GDScriptTokenizer::Token::IDENTIFIER)) { + break; } - return true; + function = parse_identifier(); } - // validate type - DataType type = _type_from_variant(p_value); - if (type.kind == DataType::BUILTIN) { - return true; - } else if (type.kind == DataType::NATIVE) { - if (ClassDB::is_parent_class(type.native_type, "Resource")) { - return true; - } + if (p_variable->property == VariableNode::PROP_SETGET) { + end_statement("property declaration"); } - _set_error("Invalid export type. Only built-in and native resource types can be exported.", p_line); - return false; -} -bool GDScriptParser::_recover_from_completion() { - if (!completion_found) { - return false; //can't recover if no completion - } - //skip stuff until newline - while (tokenizer->get_token() != GDScriptTokenizer::TK_NEWLINE && tokenizer->get_token() != GDScriptTokenizer::TK_EOF && tokenizer->get_token() != GDScriptTokenizer::TK_ERROR) { - tokenizer->advance(); - } - completion_found = false; - error_set = false; - if (tokenizer->get_token() == GDScriptTokenizer::TK_ERROR) { - error_set = true; + if (p_need_indent) { + consume(GDScriptTokenizer::Token::DEDENT, R"(Expected end of indented block for property.)"); } - - return true; + return property; } -GDScriptParser::PatternNode *GDScriptParser::_parse_pattern(bool p_static) { - PatternNode *pattern = alloc_node<PatternNode>(); - - GDScriptTokenizer::Token token = tokenizer->get_token(); - if (error_set) { - return nullptr; - } - - if (token == GDScriptTokenizer::TK_EOF) { - return nullptr; - } - - switch (token) { - // array - case GDScriptTokenizer::TK_BRACKET_OPEN: { - tokenizer->advance(); - pattern->pt_type = GDScriptParser::PatternNode::PT_ARRAY; - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_CLOSE) { - tokenizer->advance(); - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD && tokenizer->get_token(1) == GDScriptTokenizer::TK_PERIOD) { - // match everything - tokenizer->advance(2); - PatternNode *sub_pattern = alloc_node<PatternNode>(); - sub_pattern->pt_type = GDScriptParser::PatternNode::PT_IGNORE_REST; - pattern->array.push_back(sub_pattern); - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA && tokenizer->get_token(1) == GDScriptTokenizer::TK_BRACKET_CLOSE) { - tokenizer->advance(2); - break; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_CLOSE) { - tokenizer->advance(1); - break; - } else { - _set_error("'..' pattern only allowed at the end of an array pattern"); - return nullptr; - } - } - - PatternNode *sub_pattern = _parse_pattern(p_static); - if (!sub_pattern) { - return nullptr; - } - - pattern->array.push_back(sub_pattern); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - continue; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BRACKET_CLOSE) { - tokenizer->advance(); - break; - } else { - _set_error("Not a valid pattern"); - return nullptr; - } - } - } break; - // bind - case GDScriptTokenizer::TK_PR_VAR: { - tokenizer->advance(); - if (!tokenizer->is_token_literal()) { - _set_error("Expected identifier for binding variable name."); - return nullptr; +void GDScriptParser::parse_property_setter(VariableNode *p_variable) { + switch (p_variable->property) { + case VariableNode::PROP_INLINE: + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "set".)"); + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected parameter name after "(".)")) { + p_variable->setter_parameter = parse_identifier(); } - pattern->pt_type = GDScriptParser::PatternNode::PT_BIND; - pattern->bind = tokenizer->get_token_literal(); - // Check if variable name is already used - BlockNode *bl = current_block; - while (bl) { - if (bl->variables.has(pattern->bind)) { - _set_error("Binding name of '" + pattern->bind.operator String() + "' is already declared in this scope."); - return nullptr; - } - bl = bl->parent_block; - } - // Create local variable for proper identifier detection later - LocalVarNode *lv = alloc_node<LocalVarNode>(); - lv->name = pattern->bind; - current_block->variables.insert(lv->name, lv); - tokenizer->advance(); - } break; - // dictionary - case GDScriptTokenizer::TK_CURLY_BRACKET_OPEN: { - tokenizer->advance(); - pattern->pt_type = GDScriptParser::PatternNode::PT_DICTIONARY; - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - tokenizer->advance(); - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD && tokenizer->get_token(1) == GDScriptTokenizer::TK_PERIOD) { - // match everything - tokenizer->advance(2); - PatternNode *sub_pattern = alloc_node<PatternNode>(); - sub_pattern->pt_type = PatternNode::PT_IGNORE_REST; - pattern->array.push_back(sub_pattern); - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA && tokenizer->get_token(1) == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - tokenizer->advance(2); - break; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - tokenizer->advance(1); - break; - } else { - _set_error("'..' pattern only allowed at the end of a dictionary pattern"); - return nullptr; - } - } - - Node *key = _parse_and_reduce_expression(pattern, p_static); - if (!key) { - _set_error("Not a valid key in pattern"); - return nullptr; - } + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after parameter name.)*"); + consume(GDScriptTokenizer::Token::COLON, R"*(Expected ":" after ")".)*"); - if (key->type != GDScriptParser::Node::TYPE_CONSTANT) { - _set_error("Not a constant expression as key"); - return nullptr; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - tokenizer->advance(); - - PatternNode *value = _parse_pattern(p_static); - if (!value) { - _set_error("Expected pattern in dictionary value"); - return nullptr; - } - - pattern->dictionary.insert(static_cast<ConstantNode *>(key), value); - } else { - pattern->dictionary.insert(static_cast<ConstantNode *>(key), nullptr); - } + p_variable->setter = parse_suite("setter definition"); + break; - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - continue; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - tokenizer->advance(); - break; - } else { - _set_error("Not a valid pattern"); - return nullptr; - } + case VariableNode::PROP_SETGET: + consume(GDScriptTokenizer::Token::EQUAL, R"(Expected "=" after "set")"); + make_completion_context(COMPLETION_PROPERTY_METHOD, p_variable); + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected setter function name after "=".)")) { + p_variable->setter_pointer = parse_identifier(); } - } break; - case GDScriptTokenizer::TK_WILDCARD: { - tokenizer->advance(); - pattern->pt_type = PatternNode::PT_WILDCARD; - } break; - // all the constants like strings and numbers - default: { - Node *value = _parse_and_reduce_expression(pattern, p_static); - if (!value) { - _set_error("Expect constant expression or variables in a pattern"); - return nullptr; - } - - if (value->type == Node::TYPE_OPERATOR) { - // Maybe it's SomeEnum.VALUE - Node *current_value = value; - - while (current_value->type == Node::TYPE_OPERATOR) { - OperatorNode *op_node = static_cast<OperatorNode *>(current_value); - - if (op_node->op != OperatorNode::OP_INDEX_NAMED) { - _set_error("Invalid operator in pattern. Only index (`A.B`) is allowed"); - return nullptr; - } - current_value = op_node->arguments[0]; - } + break; + case VariableNode::PROP_NONE: + break; // Unreachable. + } +} - if (current_value->type != Node::TYPE_IDENTIFIER) { - _set_error("Only constant expression or variables allowed in a pattern"); - return nullptr; - } +void GDScriptParser::parse_property_getter(VariableNode *p_variable) { + switch (p_variable->property) { + case VariableNode::PROP_INLINE: + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "get".)"); - } else if (value->type != Node::TYPE_IDENTIFIER && value->type != Node::TYPE_CONSTANT) { - _set_error("Only constant expressions or variables allowed in a pattern"); - return nullptr; + p_variable->getter = parse_suite("getter definition"); + break; + case VariableNode::PROP_SETGET: + consume(GDScriptTokenizer::Token::EQUAL, R"(Expected "=" after "get")"); + make_completion_context(COMPLETION_PROPERTY_METHOD, p_variable); + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected getter function name after "=".)")) { + p_variable->getter_pointer = parse_identifier(); } - - pattern->pt_type = PatternNode::PT_CONSTANT; - pattern->constant = value; - } break; + break; + case VariableNode::PROP_NONE: + break; // Unreachable. } - - return pattern; } -void GDScriptParser::_parse_pattern_block(BlockNode *p_block, Vector<PatternBranchNode *> &p_branches, bool p_static) { - IndentLevel current_level = indent_level.back()->get(); - - p_block->has_return = true; - - bool catch_all_appeared = false; - - while (true) { - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE && _parse_newline()) { - ; - } - - // GDScriptTokenizer::Token token = tokenizer->get_token(); - if (error_set) { - return; - } - - if (current_level.indent > indent_level.back()->get().indent) { - break; // go back a level - } - - pending_newline = -1; - - PatternBranchNode *branch = alloc_node<PatternBranchNode>(); - branch->body = alloc_node<BlockNode>(); - branch->body->parent_block = p_block; - p_block->sub_blocks.push_back(branch->body); - current_block = branch->body; - - branch->patterns.push_back(_parse_pattern(p_static)); - if (!branch->patterns[0]) { - break; - } +GDScriptParser::ConstantNode *GDScriptParser::parse_constant() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) { + return nullptr; + } - bool has_binding = branch->patterns[0]->pt_type == PatternNode::PT_BIND; - bool catch_all = has_binding || branch->patterns[0]->pt_type == PatternNode::PT_WILDCARD; + ConstantNode *constant = alloc_node<ConstantNode>(); + constant->identifier = parse_identifier(); -#ifdef DEBUG_ENABLED - // Branches after a wildcard or binding are unreachable - if (catch_all_appeared && !current_function->has_unreachable_code) { - _add_warning(GDScriptWarning::UNREACHABLE_CODE, -1, current_function->name.operator String()); - current_function->has_unreachable_code = true; + if (match(GDScriptTokenizer::Token::COLON)) { + if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + constant->infer_datatype = true; + } else { + // Parse type. + constant->datatype_specifier = parse_type(); } -#endif - - while (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - branch->patterns.push_back(_parse_pattern(p_static)); - if (!branch->patterns[branch->patterns.size() - 1]) { - return; - } + } - PatternNode::PatternType pt = branch->patterns[branch->patterns.size() - 1]->pt_type; + if (consume(GDScriptTokenizer::Token::EQUAL, R"(Expected initializer after constant name.)")) { + // Initializer. + constant->initializer = parse_expression(false); - if (pt == PatternNode::PT_BIND) { - _set_error("Cannot use bindings with multipattern."); - return; - } - - catch_all = catch_all || pt == PatternNode::PT_WILDCARD; + if (constant->initializer == nullptr) { + push_error(R"(Expected initializer expression for constant.)"); + return nullptr; } + } - catch_all_appeared = catch_all_appeared || catch_all; + end_statement("constant declaration"); - if (!_enter_indent_block()) { - _set_error("Expected block in pattern branch"); - return; - } + return constant; +} - _parse_block(branch->body, p_static); +GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected parameter name.)")) { + return nullptr; + } - current_block = p_block; + ParameterNode *parameter = alloc_node<ParameterNode>(); + parameter->identifier = parse_identifier(); - if (!branch->body->has_return) { - p_block->has_return = false; + if (match(GDScriptTokenizer::Token::COLON)) { + if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + parameter->infer_datatype = true; + } else { + // Parse type. + make_completion_context(COMPLETION_TYPE_NAME, parameter); + parameter->datatype_specifier = parse_type(); } - - p_branches.push_back(branch); } - // Even if all branches return, there is possibility of default fallthrough - if (!catch_all_appeared) { - p_block->has_return = false; + if (match(GDScriptTokenizer::Token::EQUAL)) { + // Default value. + parameter->default_value = parse_expression(false); } -} -void GDScriptParser::_generate_pattern(PatternNode *p_pattern, Node *p_node_to_match, Node *&p_resulting_node, Map<StringName, Node *> &p_bindings) { - const DataType &to_match_type = p_node_to_match->get_datatype(); + return parameter; +} - switch (p_pattern->pt_type) { - case PatternNode::PT_CONSTANT: { - DataType pattern_type = _reduce_node_type(p_pattern->constant); - if (error_set) { - return; - } +GDScriptParser::SignalNode *GDScriptParser::parse_signal() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) { + return nullptr; + } - OperatorNode *type_comp = nullptr; + SignalNode *signal = alloc_node<SignalNode>(); + signal->identifier = parse_identifier(); - // static type check if possible - if (pattern_type.has_type && to_match_type.has_type) { - if (!_is_type_compatible(to_match_type, pattern_type) && !_is_type_compatible(pattern_type, to_match_type)) { - _set_error("The pattern type (" + pattern_type.to_string() + ") isn't compatible with the type of the value to match (" + to_match_type.to_string() + ").", - p_pattern->line); - return; - } - } else { - // runtime typecheck - BuiltInFunctionNode *typeof_node = alloc_node<BuiltInFunctionNode>(); - typeof_node->function = GDScriptFunctions::TYPE_OF; - - OperatorNode *typeof_match_value = alloc_node<OperatorNode>(); - typeof_match_value->op = OperatorNode::OP_CALL; - typeof_match_value->arguments.push_back(typeof_node); - typeof_match_value->arguments.push_back(p_node_to_match); - - OperatorNode *typeof_pattern_value = alloc_node<OperatorNode>(); - typeof_pattern_value->op = OperatorNode::OP_CALL; - typeof_pattern_value->arguments.push_back(typeof_node); - typeof_pattern_value->arguments.push_back(p_pattern->constant); - - type_comp = alloc_node<OperatorNode>(); - type_comp->op = OperatorNode::OP_EQUAL; - type_comp->arguments.push_back(typeof_match_value); - type_comp->arguments.push_back(typeof_pattern_value); + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + do { + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; } - // compare the actual values - OperatorNode *value_comp = alloc_node<OperatorNode>(); - value_comp->op = OperatorNode::OP_EQUAL; - value_comp->arguments.push_back(p_pattern->constant); - value_comp->arguments.push_back(p_node_to_match); - - if (type_comp) { - OperatorNode *full_comparison = alloc_node<OperatorNode>(); - full_comparison->op = OperatorNode::OP_AND; - full_comparison->arguments.push_back(type_comp); - full_comparison->arguments.push_back(value_comp); - - p_resulting_node = full_comparison; - } else { - p_resulting_node = value_comp; + ParameterNode *parameter = parse_parameter(); + if (parameter == nullptr) { + push_error("Expected signal parameter name."); + break; } - - } break; - case PatternNode::PT_BIND: { - p_bindings[p_pattern->bind] = p_node_to_match; - - // a bind always matches - ConstantNode *true_value = alloc_node<ConstantNode>(); - true_value->value = Variant(true); - true_value->datatype = _type_from_variant(true_value->value); - p_resulting_node = true_value; - } break; - case PatternNode::PT_ARRAY: { - bool open_ended = false; - - if (p_pattern->array.size() > 0) { - if (p_pattern->array[p_pattern->array.size() - 1]->pt_type == PatternNode::PT_IGNORE_REST) { - open_ended = true; - } + if (parameter->default_value != nullptr) { + push_error(R"(Signal parameters cannot have a default value.)"); } - - // typeof(value_to_match) == TYPE_ARRAY && value_to_match.size() >= length - // typeof(value_to_match) == TYPE_ARRAY && value_to_match.size() == length - - { - OperatorNode *type_comp = nullptr; - // static type check if possible - if (to_match_type.has_type) { - // must be an array - if (to_match_type.kind != DataType::BUILTIN || to_match_type.builtin_type != Variant::ARRAY) { - _set_error("Cannot match an array pattern with a non-array expression.", p_pattern->line); - return; - } - } else { - // runtime typecheck - BuiltInFunctionNode *typeof_node = alloc_node<BuiltInFunctionNode>(); - typeof_node->function = GDScriptFunctions::TYPE_OF; - - OperatorNode *typeof_match_value = alloc_node<OperatorNode>(); - typeof_match_value->op = OperatorNode::OP_CALL; - typeof_match_value->arguments.push_back(typeof_node); - typeof_match_value->arguments.push_back(p_node_to_match); - - IdentifierNode *typeof_array = alloc_node<IdentifierNode>(); - typeof_array->name = "TYPE_ARRAY"; - - type_comp = alloc_node<OperatorNode>(); - type_comp->op = OperatorNode::OP_EQUAL; - type_comp->arguments.push_back(typeof_match_value); - type_comp->arguments.push_back(typeof_array); - } - - // size - ConstantNode *length = alloc_node<ConstantNode>(); - length->value = Variant(open_ended ? p_pattern->array.size() - 1 : p_pattern->array.size()); - length->datatype = _type_from_variant(length->value); - - OperatorNode *call = alloc_node<OperatorNode>(); - call->op = OperatorNode::OP_CALL; - call->arguments.push_back(p_node_to_match); - - IdentifierNode *size = alloc_node<IdentifierNode>(); - size->name = "size"; - call->arguments.push_back(size); - - OperatorNode *length_comparison = alloc_node<OperatorNode>(); - length_comparison->op = open_ended ? OperatorNode::OP_GREATER_EQUAL : OperatorNode::OP_EQUAL; - length_comparison->arguments.push_back(call); - length_comparison->arguments.push_back(length); - - if (type_comp) { - OperatorNode *type_and_length_comparison = alloc_node<OperatorNode>(); - type_and_length_comparison->op = OperatorNode::OP_AND; - type_and_length_comparison->arguments.push_back(type_comp); - type_and_length_comparison->arguments.push_back(length_comparison); - - p_resulting_node = type_and_length_comparison; - } else { - p_resulting_node = length_comparison; - } + if (signal->parameters_indices.has(parameter->identifier->name)) { + push_error(vformat(R"(Parameter with name "%s" was already declared for this signal.)", parameter->identifier->name)); + } else { + signal->parameters_indices[parameter->identifier->name] = signal->parameters.size(); + signal->parameters.push_back(parameter); } + } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); - for (int i = 0; i < p_pattern->array.size(); i++) { - PatternNode *pattern = p_pattern->array[i]; + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after signal parameters.)*"); + } - Node *condition = nullptr; + end_statement("signal declaration"); - ConstantNode *index = alloc_node<ConstantNode>(); - index->value = Variant(i); - index->datatype = _type_from_variant(index->value); + return signal; +} - OperatorNode *indexed_value = alloc_node<OperatorNode>(); - indexed_value->op = OperatorNode::OP_INDEX; - indexed_value->arguments.push_back(p_node_to_match); - indexed_value->arguments.push_back(index); +GDScriptParser::EnumNode *GDScriptParser::parse_enum() { + EnumNode *enum_node = alloc_node<EnumNode>(); + bool named = false; - _generate_pattern(pattern, indexed_value, condition, p_bindings); + if (check(GDScriptTokenizer::Token::IDENTIFIER)) { + advance(); + enum_node->identifier = parse_identifier(); + named = true; + } - // concatenate all the patterns with && - OperatorNode *and_node = alloc_node<OperatorNode>(); - and_node->op = OperatorNode::OP_AND; - and_node->arguments.push_back(p_resulting_node); - and_node->arguments.push_back(condition); + push_multiline(true); + consume(GDScriptTokenizer::Token::BRACE_OPEN, vformat(R"(Expected "{" after %s.)", named ? "enum name" : R"("enum")")); - p_resulting_node = and_node; - } + HashMap<StringName, int> elements; - } break; - case PatternNode::PT_DICTIONARY: { - bool open_ended = false; - - if (p_pattern->array.size() > 0) { - open_ended = true; - } + do { + if (check(GDScriptTokenizer::Token::BRACE_CLOSE)) { + break; // Allow trailing comma. + } + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifer for enum key.)")) { + EnumNode::Value item; + item.identifier = parse_identifier(); + item.parent_enum = enum_node; + item.line = previous.start_line; + item.leftmost_column = previous.leftmost_column; - // typeof(value_to_match) == TYPE_DICTIONARY && value_to_match.size() >= length - // typeof(value_to_match) == TYPE_DICTIONARY && value_to_match.size() == length - - { - OperatorNode *type_comp = nullptr; - // static type check if possible - if (to_match_type.has_type) { - // must be an dictionary - if (to_match_type.kind != DataType::BUILTIN || to_match_type.builtin_type != Variant::DICTIONARY) { - _set_error("Cannot match an dictionary pattern with a non-dictionary expression.", p_pattern->line); - return; + if (elements.has(item.identifier->name)) { + push_error(vformat(R"(Name "%s" was already in this enum (at line %d).)", item.identifier->name, elements[item.identifier->name]), item.identifier); + } else if (!named) { + // TODO: Abstract this recursive member check. + ClassNode *parent = current_class; + while (parent != nullptr) { + if (parent->members_indices.has(item.identifier->name)) { + push_error(vformat(R"(Name "%s" is already used as a class %s.)", item.identifier->name, parent->get_member(item.identifier->name).get_type_name())); + break; } - } else { - // runtime typecheck - BuiltInFunctionNode *typeof_node = alloc_node<BuiltInFunctionNode>(); - typeof_node->function = GDScriptFunctions::TYPE_OF; - - OperatorNode *typeof_match_value = alloc_node<OperatorNode>(); - typeof_match_value->op = OperatorNode::OP_CALL; - typeof_match_value->arguments.push_back(typeof_node); - typeof_match_value->arguments.push_back(p_node_to_match); - - IdentifierNode *typeof_dictionary = alloc_node<IdentifierNode>(); - typeof_dictionary->name = "TYPE_DICTIONARY"; - - type_comp = alloc_node<OperatorNode>(); - type_comp->op = OperatorNode::OP_EQUAL; - type_comp->arguments.push_back(typeof_match_value); - type_comp->arguments.push_back(typeof_dictionary); + parent = parent->outer; } + } - // size - ConstantNode *length = alloc_node<ConstantNode>(); - length->value = Variant(open_ended ? p_pattern->dictionary.size() - 1 : p_pattern->dictionary.size()); - length->datatype = _type_from_variant(length->value); - - OperatorNode *call = alloc_node<OperatorNode>(); - call->op = OperatorNode::OP_CALL; - call->arguments.push_back(p_node_to_match); - - IdentifierNode *size = alloc_node<IdentifierNode>(); - size->name = "size"; - call->arguments.push_back(size); - - OperatorNode *length_comparison = alloc_node<OperatorNode>(); - length_comparison->op = open_ended ? OperatorNode::OP_GREATER_EQUAL : OperatorNode::OP_EQUAL; - length_comparison->arguments.push_back(call); - length_comparison->arguments.push_back(length); - - if (type_comp) { - OperatorNode *type_and_length_comparison = alloc_node<OperatorNode>(); - type_and_length_comparison->op = OperatorNode::OP_AND; - type_and_length_comparison->arguments.push_back(type_comp); - type_and_length_comparison->arguments.push_back(length_comparison); + elements[item.identifier->name] = item.line; - p_resulting_node = type_and_length_comparison; - } else { - p_resulting_node = length_comparison; + if (match(GDScriptTokenizer::Token::EQUAL)) { + ExpressionNode *value = parse_expression(false); + if (value == nullptr) { + push_error(R"(Expected expression value after "=".)"); } + item.custom_value = value; } + item.rightmost_column = previous.rightmost_column; - for (Map<ConstantNode *, PatternNode *>::Element *e = p_pattern->dictionary.front(); e; e = e->next()) { - Node *condition = nullptr; - - // check for has, then for pattern - - IdentifierNode *has = alloc_node<IdentifierNode>(); - has->name = "has"; - - OperatorNode *has_call = alloc_node<OperatorNode>(); - has_call->op = OperatorNode::OP_CALL; - has_call->arguments.push_back(p_node_to_match); - has_call->arguments.push_back(has); - has_call->arguments.push_back(e->key()); - - if (e->value()) { - OperatorNode *indexed_value = alloc_node<OperatorNode>(); - indexed_value->op = OperatorNode::OP_INDEX; - indexed_value->arguments.push_back(p_node_to_match); - indexed_value->arguments.push_back(e->key()); - - _generate_pattern(e->value(), indexed_value, condition, p_bindings); - - OperatorNode *has_and_pattern = alloc_node<OperatorNode>(); - has_and_pattern->op = OperatorNode::OP_AND; - has_and_pattern->arguments.push_back(has_call); - has_and_pattern->arguments.push_back(condition); - - condition = has_and_pattern; - - } else { - condition = has_call; - } + item.index = enum_node->values.size(); + enum_node->values.push_back(item); + if (!named) { + // Add as member of current class. + current_class->add_member(item); + } + } + } while (match(GDScriptTokenizer::Token::COMMA)); - // concatenate all the patterns with && - OperatorNode *and_node = alloc_node<OperatorNode>(); - and_node->op = OperatorNode::OP_AND; - and_node->arguments.push_back(p_resulting_node); - and_node->arguments.push_back(condition); + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); - p_resulting_node = and_node; - } + end_statement("enum"); - } break; - case PatternNode::PT_IGNORE_REST: - case PatternNode::PT_WILDCARD: { - // simply generate a `true` - ConstantNode *true_value = alloc_node<ConstantNode>(); - true_value->value = Variant(true); - true_value->datatype = _type_from_variant(true_value->value); - p_resulting_node = true_value; - } break; - default: { - } break; - } + return enum_node; } -void GDScriptParser::_transform_match_statment(MatchNode *p_match_statement) { - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = "#match_value"; - id->line = p_match_statement->line; - id->datatype = _reduce_node_type(p_match_statement->val_to_match); - if (id->datatype.has_type) { - _mark_line_as_safe(id->line); - } else { - _mark_line_as_unsafe(id->line); +GDScriptParser::FunctionNode *GDScriptParser::parse_function() { + bool _static = false; + if (previous.type == GDScriptTokenizer::Token::STATIC) { + // TODO: Improve message if user uses "static" with "var" or "const" + if (!consume(GDScriptTokenizer::Token::FUNC, R"(Expected "func" after "static".)")) { + return nullptr; + } + _static = true; } - if (error_set) { - return; - } + FunctionNode *function = alloc_node<FunctionNode>(); + make_completion_context(COMPLETION_OVERRIDE_METHOD, function); - for (int i = 0; i < p_match_statement->branches.size(); i++) { - PatternBranchNode *branch = p_match_statement->branches[i]; + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) { + return nullptr; + } - MatchNode::CompiledPatternBranch compiled_branch; - compiled_branch.compiled_pattern = nullptr; + FunctionNode *previous_function = current_function; + current_function = function; - Map<StringName, Node *> binding; + function->identifier = parse_identifier(); + function->is_static = _static; - for (int j = 0; j < branch->patterns.size(); j++) { - PatternNode *pattern = branch->patterns[j]; - _mark_line_as_safe(pattern->line); + push_multiline(true); + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)"); - Map<StringName, Node *> bindings; - Node *resulting_node = nullptr; - _generate_pattern(pattern, id, resulting_node, bindings); + SuiteNode *body = alloc_node<SuiteNode>(); + SuiteNode *previous_suite = current_suite; + current_suite = body; - if (!resulting_node) { - return; + if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { + bool default_used = false; + do { + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; } - - if (!binding.empty() && !bindings.empty()) { - _set_error("Multipatterns can't contain bindings"); - return; - } else { - binding = bindings; + ParameterNode *parameter = parse_parameter(); + if (parameter == nullptr) { + break; } - - // Result is always a boolean - DataType resulting_node_type; - resulting_node_type.has_type = true; - resulting_node_type.is_constant = true; - resulting_node_type.kind = DataType::BUILTIN; - resulting_node_type.builtin_type = Variant::BOOL; - resulting_node->set_datatype(resulting_node_type); - - if (compiled_branch.compiled_pattern) { - OperatorNode *or_node = alloc_node<OperatorNode>(); - or_node->op = OperatorNode::OP_OR; - or_node->arguments.push_back(compiled_branch.compiled_pattern); - or_node->arguments.push_back(resulting_node); - - compiled_branch.compiled_pattern = or_node; + if (parameter->default_value != nullptr) { + default_used = true; } else { - // single pattern | first one - compiled_branch.compiled_pattern = resulting_node; + if (default_used) { + push_error("Cannot have a mandatory parameters after optional parameters."); + continue; + } } - } - - // prepare the body ...hehe - for (Map<StringName, Node *>::Element *e = binding.front(); e; e = e->next()) { - if (!branch->body->variables.has(e->key())) { - _set_error("Parser bug: missing pattern bind variable.", branch->line); - ERR_FAIL(); + if (function->parameters_indices.has(parameter->identifier->name)) { + push_error(vformat(R"(Parameter with name "%s" was already declared for this function.)", parameter->identifier->name)); + } else { + function->parameters_indices[parameter->identifier->name] = function->parameters.size(); + function->parameters.push_back(parameter); + body->add_local(parameter); } - - LocalVarNode *local_var = branch->body->variables[e->key()]; - local_var->assign = e->value(); - local_var->set_datatype(local_var->assign->get_datatype()); - local_var->assignments++; - - IdentifierNode *id2 = alloc_node<IdentifierNode>(); - id2->name = local_var->name; - id2->datatype = local_var->datatype; - id2->declared_block = branch->body; - id2->set_datatype(local_var->assign->get_datatype()); - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_ASSIGN; - op->arguments.push_back(id2); - op->arguments.push_back(local_var->assign); - local_var->assign_op = op; - - branch->body->statements.push_front(op); - branch->body->statements.push_front(local_var); - } - - compiled_branch.body = branch->body; - - p_match_statement->compiled_pattern_branches.push_back(compiled_branch); + } while (match(GDScriptTokenizer::Token::COMMA)); } -} - -void GDScriptParser::_parse_block(BlockNode *p_block, bool p_static) { - IndentLevel current_level = indent_level.back()->get(); - -#ifdef DEBUG_ENABLED - - pending_newline = -1; // reset for the new block - - NewLineNode *nl = alloc_node<NewLineNode>(); - - nl->line = tokenizer->get_token_line(); - p_block->statements.push_back(nl); -#endif - - bool is_first_line = true; - - while (true) { - if (!is_first_line && indent_level.back()->prev() && indent_level.back()->prev()->get().indent == current_level.indent) { - if (indent_level.back()->prev()->get().is_mixed(current_level)) { - _set_error("Mixed tabs and spaces in indentation."); - return; - } - // pythonic single-line expression, don't parse future lines - indent_level.pop_back(); - p_block->end_line = tokenizer->get_token_line(); - return; - } - is_first_line = false; - - GDScriptTokenizer::Token token = tokenizer->get_token(); - if (error_set) { - return; - } - - if (current_level.indent > indent_level.back()->get().indent) { - p_block->end_line = tokenizer->get_token_line(); - return; //go back a level - } - if (pending_newline != -1) { - NewLineNode *nl2 = alloc_node<NewLineNode>(); - nl2->line = pending_newline; - p_block->statements.push_back(nl2); - pending_newline = -1; - } + pop_multiline(); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after function parameters.)*"); -#ifdef DEBUG_ENABLED - switch (token) { - case GDScriptTokenizer::TK_EOF: - case GDScriptTokenizer::TK_ERROR: - case GDScriptTokenizer::TK_NEWLINE: - case GDScriptTokenizer::TK_CF_PASS: { - // will check later - } break; - default: { - if (p_block->has_return && !current_function->has_unreachable_code) { - _add_warning(GDScriptWarning::UNREACHABLE_CODE, -1, current_function->name.operator String()); - current_function->has_unreachable_code = true; - } - } break; + if (match(GDScriptTokenizer::Token::FORWARD_ARROW)) { + make_completion_context(COMPLETION_TYPE_NAME_OR_VOID, function); + function->return_type = parse_type(true); + if (function->return_type == nullptr) { + push_error(R"(Expected return type or "void" after "->".)"); } -#endif // DEBUG_ENABLED - switch (token) { - case GDScriptTokenizer::TK_EOF: { - p_block->end_line = tokenizer->get_token_line(); - return; // End of file! - } break; - case GDScriptTokenizer::TK_ERROR: { - return; - } break; - case GDScriptTokenizer::TK_NEWLINE: { - int line = tokenizer->get_token_line(); - - if (!_parse_newline()) { - if (!error_set) { - p_block->end_line = tokenizer->get_token_line(); - pending_newline = p_block->end_line; - } - return; - } - - _mark_line_as_safe(line); - NewLineNode *nl2 = alloc_node<NewLineNode>(); - nl2->line = line; - p_block->statements.push_back(nl2); - - } break; - case GDScriptTokenizer::TK_CF_PASS: { - if (tokenizer->get_token(1) != GDScriptTokenizer::TK_SEMICOLON && tokenizer->get_token(1) != GDScriptTokenizer::TK_NEWLINE && tokenizer->get_token(1) != GDScriptTokenizer::TK_EOF) { - _set_error("Expected \";\" or a line break."); - return; - } - _mark_line_as_safe(tokenizer->get_token_line()); - tokenizer->advance(); - if (tokenizer->get_token() == GDScriptTokenizer::TK_SEMICOLON) { - // Ignore semicolon after 'pass'. - tokenizer->advance(); - } - } break; - case GDScriptTokenizer::TK_PR_VAR: { - // Variable declaration and (eventual) initialization. - - tokenizer->advance(); - int var_line = tokenizer->get_token_line(); - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Expected an identifier for the local variable name."); - return; - } - StringName n = tokenizer->get_token_literal(); - tokenizer->advance(); - if (current_function) { - for (int i = 0; i < current_function->arguments.size(); i++) { - if (n == current_function->arguments[i]) { - _set_error("Variable \"" + String(n) + "\" already defined in the scope (at line " + itos(current_function->line) + ")."); - return; - } - } - } - BlockNode *check_block = p_block; - while (check_block) { - if (check_block->variables.has(n)) { - _set_error("Variable \"" + String(n) + "\" already defined in the scope (at line " + itos(check_block->variables[n]->line) + ")."); - return; - } - check_block = check_block->parent_block; - } - - //must know when the local variable is declared - LocalVarNode *lv = alloc_node<LocalVarNode>(); - lv->name = n; - lv->line = var_line; - p_block->statements.push_back(lv); - - Node *assigned = nullptr; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - lv->datatype = DataType(); -#ifdef DEBUG_ENABLED - lv->datatype.infer_type = true; -#endif - tokenizer->advance(); - } else if (!_parse_type(lv->datatype)) { - _set_error("Expected a type for the variable."); - return; - } - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_ASSIGN) { - tokenizer->advance(); - Node *subexpr = _parse_and_reduce_expression(p_block, p_static); - if (!subexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - - lv->assignments++; - assigned = subexpr; - } else { - assigned = _get_default_value_for_type(lv->datatype, var_line); - } - //must be added later, to avoid self-referencing. - p_block->variables.insert(n, lv); - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = n; - id->declared_block = p_block; - id->line = var_line; - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_ASSIGN; - op->arguments.push_back(id); - op->arguments.push_back(assigned); - op->line = var_line; - p_block->statements.push_back(op); - lv->assign_op = op; - lv->assign = assigned; - - if (!_end_statement()) { - _set_end_statement_error("var"); - return; - } - - } break; - case GDScriptTokenizer::TK_CF_IF: { - tokenizer->advance(); - - Node *condition = _parse_and_reduce_expression(p_block, p_static); - if (!condition) { - if (_recover_from_completion()) { - break; - } - return; - } - - ControlFlowNode *cf_if = alloc_node<ControlFlowNode>(); - - cf_if->cf_type = ControlFlowNode::CF_IF; - cf_if->arguments.push_back(condition); - - cf_if->body = alloc_node<BlockNode>(); - cf_if->body->parent_block = p_block; - cf_if->body->if_condition = condition; //helps code completion - - p_block->sub_blocks.push_back(cf_if->body); - - if (!_enter_indent_block(cf_if->body)) { - _set_error("Expected an indented block after \"if\"."); - p_block->end_line = tokenizer->get_token_line(); - return; - } - - current_block = cf_if->body; - _parse_block(cf_if->body, p_static); - current_block = p_block; - - if (error_set) { - return; - } - p_block->statements.push_back(cf_if); - - bool all_have_return = cf_if->body->has_return; - bool have_else = false; - - while (true) { - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE && _parse_newline()) { - ; - } - - if (indent_level.back()->get().indent < current_level.indent) { //not at current indent level - p_block->end_line = tokenizer->get_token_line(); - return; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CF_ELIF) { - if (indent_level.back()->get().indent > current_level.indent) { - _set_error("Invalid indentation."); - return; - } - - tokenizer->advance(); - - cf_if->body_else = alloc_node<BlockNode>(); - cf_if->body_else->parent_block = p_block; - p_block->sub_blocks.push_back(cf_if->body_else); - - ControlFlowNode *cf_else = alloc_node<ControlFlowNode>(); - cf_else->cf_type = ControlFlowNode::CF_IF; - - //condition - Node *condition2 = _parse_and_reduce_expression(p_block, p_static); - if (!condition2) { - if (_recover_from_completion()) { - break; - } - return; - } - cf_else->arguments.push_back(condition2); - cf_else->cf_type = ControlFlowNode::CF_IF; - - cf_if->body_else->statements.push_back(cf_else); - cf_if = cf_else; - cf_if->body = alloc_node<BlockNode>(); - cf_if->body->parent_block = p_block; - p_block->sub_blocks.push_back(cf_if->body); - - if (!_enter_indent_block(cf_if->body)) { - _set_error("Expected an indented block after \"elif\"."); - p_block->end_line = tokenizer->get_token_line(); - return; - } - - current_block = cf_else->body; - _parse_block(cf_else->body, p_static); - current_block = p_block; - if (error_set) { - return; - } - - all_have_return = all_have_return && cf_else->body->has_return; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CF_ELSE) { - if (indent_level.back()->get().indent > current_level.indent) { - _set_error("Invalid indentation."); - return; - } - - tokenizer->advance(); - cf_if->body_else = alloc_node<BlockNode>(); - cf_if->body_else->parent_block = p_block; - p_block->sub_blocks.push_back(cf_if->body_else); - - if (!_enter_indent_block(cf_if->body_else)) { - _set_error("Expected an indented block after \"else\"."); - p_block->end_line = tokenizer->get_token_line(); - return; - } - current_block = cf_if->body_else; - _parse_block(cf_if->body_else, p_static); - current_block = p_block; - if (error_set) { - return; - } - - all_have_return = all_have_return && cf_if->body_else->has_return; - have_else = true; - - break; //after else, exit - - } else { - break; - } - } - - cf_if->body->has_return = all_have_return; - // If there's no else block, path out of the if might not have a return - p_block->has_return = all_have_return && have_else; - - } break; - case GDScriptTokenizer::TK_CF_WHILE: { - tokenizer->advance(); - Node *condition2 = _parse_and_reduce_expression(p_block, p_static); - if (!condition2) { - if (_recover_from_completion()) { - break; - } - return; - } - - ControlFlowNode *cf_while = alloc_node<ControlFlowNode>(); - - cf_while->cf_type = ControlFlowNode::CF_WHILE; - cf_while->arguments.push_back(condition2); - - cf_while->body = alloc_node<BlockNode>(); - cf_while->body->parent_block = p_block; - cf_while->body->can_break = true; - cf_while->body->can_continue = true; - p_block->sub_blocks.push_back(cf_while->body); - - if (!_enter_indent_block(cf_while->body)) { - _set_error("Expected an indented block after \"while\"."); - p_block->end_line = tokenizer->get_token_line(); - return; - } - - current_block = cf_while->body; - _parse_block(cf_while->body, p_static); - current_block = p_block; - if (error_set) { - return; - } - p_block->statements.push_back(cf_while); - } break; - case GDScriptTokenizer::TK_CF_FOR: { - tokenizer->advance(); - - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Identifier expected after \"for\"."); - } - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = tokenizer->get_token_identifier(); -#ifdef DEBUG_ENABLED - for (int j = 0; j < current_class->variables.size(); j++) { - if (current_class->variables[j].identifier == id->name) { - _add_warning(GDScriptWarning::SHADOWED_VARIABLE, id->line, id->name, itos(current_class->variables[j].line)); - } - } -#endif // DEBUG_ENABLED - - BlockNode *check_block = p_block; - while (check_block) { - if (check_block->variables.has(id->name)) { - _set_error("Variable \"" + String(id->name) + "\" already defined in the scope (at line " + itos(check_block->variables[id->name]->line) + ")."); - return; - } - check_block = check_block->parent_block; - } - - tokenizer->advance(); - - if (tokenizer->get_token() != GDScriptTokenizer::TK_OP_IN) { - _set_error("\"in\" expected after identifier."); - return; - } - - tokenizer->advance(); - - Node *container = _parse_and_reduce_expression(p_block, p_static); - if (!container) { - if (_recover_from_completion()) { - break; - } - return; - } - - DataType iter_type; - - if (container->type == Node::TYPE_OPERATOR) { - OperatorNode *op = static_cast<OperatorNode *>(container); - if (op->op == OperatorNode::OP_CALL && op->arguments[0]->type == Node::TYPE_BUILT_IN_FUNCTION && static_cast<BuiltInFunctionNode *>(op->arguments[0])->function == GDScriptFunctions::GEN_RANGE) { - //iterating a range, so see if range() can be optimized without allocating memory, by replacing it by vectors (which can work as iterable too!) - - Vector<Node *> args; - Vector<double> constants; - - bool constant = true; - - for (int i = 1; i < op->arguments.size(); i++) { - args.push_back(op->arguments[i]); - if (op->arguments[i]->type == Node::TYPE_CONSTANT) { - ConstantNode *c = static_cast<ConstantNode *>(op->arguments[i]); - if (c->value.get_type() == Variant::FLOAT || c->value.get_type() == Variant::INT) { - constants.push_back(c->value); - } else { - constant = false; - } - } else { - constant = false; - } - } - - if (args.size() > 0 && args.size() < 4) { - if (constant) { - ConstantNode *cn = alloc_node<ConstantNode>(); - switch (args.size()) { - case 1: - cn->value = (int64_t)constants[0]; - break; - case 2: - cn->value = Vector2i(constants[0], constants[1]); - break; - case 3: - cn->value = Vector3i(constants[0], constants[1], constants[2]); - break; - } - cn->datatype = _type_from_variant(cn->value); - container = cn; - } else { - OperatorNode *on = alloc_node<OperatorNode>(); - on->op = OperatorNode::OP_CALL; - - TypeNode *tn = alloc_node<TypeNode>(); - on->arguments.push_back(tn); - - switch (args.size()) { - case 1: - tn->vtype = Variant::INT; - break; - case 2: - tn->vtype = Variant::VECTOR2I; - break; - case 3: - tn->vtype = Variant::VECTOR3I; - break; - } - - for (int i = 0; i < args.size(); i++) { - on->arguments.push_back(args[i]); - } - - container = on; - } - } - - iter_type.has_type = true; - iter_type.kind = DataType::BUILTIN; - iter_type.builtin_type = Variant::INT; - } - } - - ControlFlowNode *cf_for = alloc_node<ControlFlowNode>(); - - cf_for->cf_type = ControlFlowNode::CF_FOR; - cf_for->arguments.push_back(id); - cf_for->arguments.push_back(container); - - cf_for->body = alloc_node<BlockNode>(); - cf_for->body->parent_block = p_block; - cf_for->body->can_break = true; - cf_for->body->can_continue = true; - p_block->sub_blocks.push_back(cf_for->body); - - if (!_enter_indent_block(cf_for->body)) { - _set_error("Expected indented block after \"for\"."); - p_block->end_line = tokenizer->get_token_line(); - return; - } - - current_block = cf_for->body; - - // this is for checking variable for redefining - // inside this _parse_block - LocalVarNode *lv = alloc_node<LocalVarNode>(); - lv->name = id->name; - lv->line = id->line; - lv->assignments++; - id->declared_block = cf_for->body; - lv->set_datatype(iter_type); - id->set_datatype(iter_type); - cf_for->body->variables.insert(id->name, lv); - _parse_block(cf_for->body, p_static); - current_block = p_block; - - if (error_set) { - return; - } - p_block->statements.push_back(cf_for); - } break; - case GDScriptTokenizer::TK_CF_CONTINUE: { - BlockNode *upper_block = p_block; - bool is_continue_valid = false; - while (upper_block) { - if (upper_block->can_continue) { - is_continue_valid = true; - break; - } - upper_block = upper_block->parent_block; - } - - if (!is_continue_valid) { - _set_error("Unexpected keyword \"continue\" outside a loop."); - return; - } - - _mark_line_as_safe(tokenizer->get_token_line()); - tokenizer->advance(); - ControlFlowNode *cf_continue = alloc_node<ControlFlowNode>(); - cf_continue->cf_type = ControlFlowNode::CF_CONTINUE; - p_block->statements.push_back(cf_continue); - if (!_end_statement()) { - _set_end_statement_error("continue"); - return; - } - } break; - case GDScriptTokenizer::TK_CF_BREAK: { - BlockNode *upper_block = p_block; - bool is_break_valid = false; - while (upper_block) { - if (upper_block->can_break) { - is_break_valid = true; - break; - } - upper_block = upper_block->parent_block; - } - - if (!is_break_valid) { - _set_error("Unexpected keyword \"break\" outside a loop."); - return; - } - - _mark_line_as_safe(tokenizer->get_token_line()); - tokenizer->advance(); - ControlFlowNode *cf_break = alloc_node<ControlFlowNode>(); - cf_break->cf_type = ControlFlowNode::CF_BREAK; - p_block->statements.push_back(cf_break); - if (!_end_statement()) { - _set_end_statement_error("break"); - return; - } - } break; - case GDScriptTokenizer::TK_CF_RETURN: { - tokenizer->advance(); - ControlFlowNode *cf_return = alloc_node<ControlFlowNode>(); - cf_return->cf_type = ControlFlowNode::CF_RETURN; - cf_return->line = tokenizer->get_token_line(-1); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_SEMICOLON || tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE || tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - //expect end of statement - p_block->statements.push_back(cf_return); - if (!_end_statement()) { - return; - } - } else { - //expect expression - Node *retexpr = _parse_and_reduce_expression(p_block, p_static); - if (!retexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - cf_return->arguments.push_back(retexpr); - p_block->statements.push_back(cf_return); - if (!_end_statement()) { - _set_end_statement_error("return"); - return; - } - } - p_block->has_return = true; - - } break; - case GDScriptTokenizer::TK_CF_MATCH: { - tokenizer->advance(); - - MatchNode *match_node = alloc_node<MatchNode>(); - - Node *val_to_match = _parse_and_reduce_expression(p_block, p_static); - - if (!val_to_match) { - if (_recover_from_completion()) { - break; - } - return; - } - - match_node->val_to_match = val_to_match; - - if (!_enter_indent_block()) { - _set_error("Expected indented pattern matching block after \"match\"."); - return; - } - - BlockNode *compiled_branches = alloc_node<BlockNode>(); - compiled_branches->parent_block = p_block; - compiled_branches->parent_class = p_block->parent_class; - compiled_branches->can_continue = true; - - p_block->sub_blocks.push_back(compiled_branches); - - _parse_pattern_block(compiled_branches, match_node->branches, p_static); - - if (error_set) { - return; - } - - ControlFlowNode *match_cf_node = alloc_node<ControlFlowNode>(); - match_cf_node->cf_type = ControlFlowNode::CF_MATCH; - match_cf_node->match = match_node; - match_cf_node->body = compiled_branches; + } - p_block->has_return = p_block->has_return || compiled_branches->has_return; - p_block->statements.push_back(match_cf_node); + // TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens. + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after function declaration.)"); - _end_statement(); - } break; - case GDScriptTokenizer::TK_PR_ASSERT: { - tokenizer->advance(); + current_suite = previous_suite; + function->body = parse_suite("function declaration", body); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected '(' after assert"); - return; - } + current_function = previous_function; + return function; +} - int assert_line = tokenizer->get_token_line(); +GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_valid_targets) { + AnnotationNode *annotation = alloc_node<AnnotationNode>(); - tokenizer->advance(); + annotation->name = previous.literal; - Vector<Node *> args; - const bool result = _parse_arguments(p_block, args, p_static); - if (!result) { - return; - } + make_completion_context(COMPLETION_ANNOTATION, annotation); - if (args.empty() || args.size() > 2) { - _set_error("Wrong number of arguments, expected 1 or 2", assert_line); - return; - } + bool valid = true; - AssertNode *an = alloc_node<AssertNode>(); - an->condition = _reduce_expression(args[0], p_static); - an->line = assert_line; + if (!valid_annotations.has(annotation->name)) { + push_error(vformat(R"(Unrecognized annotation: "%s".)", annotation->name)); + valid = false; + } - if (args.size() == 2) { - an->message = _reduce_expression(args[1], p_static); - } else { - ConstantNode *message_node = alloc_node<ConstantNode>(); - message_node->value = String(); - message_node->datatype = _type_from_variant(message_node->value); - an->message = message_node; - } + annotation->info = &valid_annotations[annotation->name]; - p_block->statements.push_back(an); + if (!annotation->applies_to(p_valid_targets)) { + push_error(vformat(R"(Annotation "%s" is not allowed in this level.)", annotation->name)); + valid = false; + } - if (!_end_statement()) { - _set_end_statement_error("assert"); - return; - } - } break; - case GDScriptTokenizer::TK_PR_BREAKPOINT: { - tokenizer->advance(); - BreakpointNode *bn = alloc_node<BreakpointNode>(); - p_block->statements.push_back(bn); - - if (!_end_statement()) { - _set_end_statement_error("breakpoint"); - return; - } - } break; - default: { - Node *expression = _parse_and_reduce_expression(p_block, p_static, false, true); - if (!expression) { - if (_recover_from_completion()) { - break; - } - return; - } - p_block->statements.push_back(expression); - if (!_end_statement()) { - // Attempt to guess a better error message if the user "retypes" a variable - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON && tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - _set_error("Unexpected ':=', use '=' instead. Expected end of statement after expression."); - } else { - _set_error(vformat("Expected end of statement after expression, got %s instead.", tokenizer->get_token_name(tokenizer->get_token()))); - } - return; + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + // Arguments. + push_completion_call(annotation); + make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, 0, true); + if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { + int argument_index = 0; + do { + make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index, true); + set_last_completion_call_arg(argument_index++); + ExpressionNode *argument = parse_expression(false); + if (argument == nullptr) { + valid = false; + continue; } + annotation->arguments.push_back(argument); + } while (match(GDScriptTokenizer::Token::COMMA)); - } break; + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after annotation arguments.)*"); } + pop_completion_call(); } -} - -bool GDScriptParser::_parse_newline() { - if (tokenizer->get_token(1) != GDScriptTokenizer::TK_EOF && tokenizer->get_token(1) != GDScriptTokenizer::TK_NEWLINE) { - IndentLevel current_level = indent_level.back()->get(); - int indent = tokenizer->get_token_line_indent(); - int tabs = tokenizer->get_token_line_tab_indent(); - IndentLevel new_level(indent, tabs); - - if (new_level.is_mixed(current_level)) { - _set_error("Mixed tabs and spaces in indentation."); - return false; - } - - if (indent > current_level.indent) { - _set_error("Unexpected indentation."); - return false; - } - - if (indent < current_level.indent) { - while (indent < current_level.indent) { - //exit block - if (indent_level.size() == 1) { - _set_error("Invalid indentation. Bug?"); - return false; - } - - indent_level.pop_back(); - - if (indent_level.back()->get().indent < indent) { - _set_error("Unindent does not match any outer indentation level."); - return false; - } - - if (indent_level.back()->get().is_mixed(current_level)) { - _set_error("Mixed tabs and spaces in indentation."); - return false; - } - current_level = indent_level.back()->get(); - } + match(GDScriptTokenizer::Token::NEWLINE); // Newline after annotation is optional. - tokenizer->advance(); - return false; - } + if (valid) { + valid = validate_annotation_arguments(annotation); } - tokenizer->advance(); - return true; + return valid ? annotation : nullptr; } -void GDScriptParser::_parse_extends(ClassNode *p_class) { - if (p_class->extends_used) { - _set_error("\"extends\" can only be present once per script."); - return; - } - - if (!p_class->constant_expressions.empty() || !p_class->subclasses.empty() || !p_class->functions.empty() || !p_class->variables.empty()) { - _set_error("\"extends\" must be used before anything else."); - return; +void GDScriptParser::clear_unused_annotations() { + for (const List<AnnotationNode *>::Element *E = annotation_stack.front(); E != nullptr; E = E->next()) { + AnnotationNode *annotation = E->get(); + push_error(vformat(R"(Annotation "%s" does not precedes a valid target, so it will have no effect.)", annotation->name), annotation); } - p_class->extends_used = true; + annotation_stack.clear(); +} - tokenizer->advance(); +bool GDScriptParser::register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, int p_optional_arguments, bool p_is_vararg) { + ERR_FAIL_COND_V_MSG(valid_annotations.has(p_info.name), false, vformat(R"(Annotation "%s" already registered.)", p_info.name)); - if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE && tokenizer->get_token_type() == Variant::OBJECT) { - p_class->extends_class.push_back(Variant::get_type_name(Variant::OBJECT)); - tokenizer->advance(); - return; + AnnotationInfo new_annotation; + new_annotation.info = p_info; + new_annotation.info.default_arguments.resize(p_optional_arguments); + if (p_is_vararg) { + new_annotation.info.flags |= METHOD_FLAG_VARARG; } + new_annotation.apply = p_apply; + new_annotation.target_kind = p_target_kinds; - // see if inheritance happens from a file - if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT) { - Variant constant = tokenizer->get_token_constant(); - if (constant.get_type() != Variant::STRING) { - _set_error("\"extends\" constant must be a string."); - return; - } + valid_annotations[p_info.name] = new_annotation; + return true; +} - p_class->extends_file = constant; - tokenizer->advance(); +GDScriptParser::SuiteNode *GDScriptParser::parse_suite(const String &p_context, SuiteNode *p_suite) { + SuiteNode *suite = p_suite != nullptr ? p_suite : alloc_node<SuiteNode>(); + suite->parent_block = current_suite; + current_suite = suite; - // Add parent script as a dependency - String parent = constant; - if (parent.is_rel_path()) { - parent = base_path.plus_file(parent).simplify_path(); - } - dependencies.push_back(parent); + bool multiline = false; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PERIOD) { - return; - } else { - tokenizer->advance(); - } + if (check(GDScriptTokenizer::Token::NEWLINE)) { + multiline = true; } - while (true) { - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_IDENTIFIER: { - StringName identifier = tokenizer->get_token_identifier(); - p_class->extends_class.push_back(identifier); - } break; - - case GDScriptTokenizer::TK_PERIOD: - break; + if (multiline) { + consume(GDScriptTokenizer::Token::NEWLINE, vformat(R"(Expected newline after %s.)", p_context)); - default: { - _set_error("Invalid \"extends\" syntax, expected string constant (path) and/or identifier (parent class)."); - return; - } - } - - tokenizer->advance(1); - - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_IDENTIFIER: - case GDScriptTokenizer::TK_PERIOD: - continue; - - default: - return; + if (!consume(GDScriptTokenizer::Token::INDENT, vformat(R"(Expected indented block after %s.)", p_context))) { + current_suite = suite->parent_block; + return suite; } } -} -void GDScriptParser::_parse_class(ClassNode *p_class) { - IndentLevel current_level = indent_level.back()->get(); - - while (true) { - GDScriptTokenizer::Token token = tokenizer->get_token(); - if (error_set) { - return; - } - - if (current_level.indent > indent_level.back()->get().indent) { - p_class->end_line = tokenizer->get_token_line(); - return; //go back a level + do { + Node *statement = parse_statement(); + if (statement == nullptr) { + continue; } + suite->statements.push_back(statement); - switch (token) { - case GDScriptTokenizer::TK_CURSOR: { - tokenizer->advance(); - } break; - case GDScriptTokenizer::TK_EOF: { - p_class->end_line = tokenizer->get_token_line(); - return; // End of file! - } break; - case GDScriptTokenizer::TK_ERROR: { - return; // Go back. - } break; - case GDScriptTokenizer::TK_NEWLINE: { - if (!_parse_newline()) { - if (!error_set) { - p_class->end_line = tokenizer->get_token_line(); - } - return; - } - } break; - case GDScriptTokenizer::TK_PR_EXTENDS: { - _mark_line_as_safe(tokenizer->get_token_line()); - _parse_extends(p_class); - if (error_set) { - return; - } - if (!_end_statement()) { - _set_end_statement_error("extends"); - return; - } - - } break; - case GDScriptTokenizer::TK_PR_CLASS_NAME: { - _mark_line_as_safe(tokenizer->get_token_line()); - if (p_class->owner) { - _set_error("\"class_name\" is only valid for the main class namespace."); - return; - } - if (self_path.begins_with("res://") && self_path.find("::") != -1) { - _set_error("\"class_name\" isn't allowed in built-in scripts."); - return; - } - if (tokenizer->get_token(1) != GDScriptTokenizer::TK_IDENTIFIER) { - _set_error("\"class_name\" syntax: \"class_name <UniqueName>\""); - return; - } - if (p_class->classname_used) { - _set_error("\"class_name\" can only be present once per script."); - return; - } - - p_class->classname_used = true; - - p_class->name = tokenizer->get_token_identifier(1); - - if (self_path != String() && ScriptServer::is_global_class(p_class->name) && ScriptServer::get_global_class_path(p_class->name) != self_path) { - _set_error("Unique global class \"" + p_class->name + "\" already exists at path: " + ScriptServer::get_global_class_path(p_class->name)); - return; - } - - if (ClassDB::class_exists(p_class->name)) { - _set_error("The class \"" + p_class->name + "\" shadows a native class."); - return; - } - - if (p_class->classname_used && ProjectSettings::get_singleton()->has_setting("autoload/" + p_class->name)) { - const String autoload_path = ProjectSettings::get_singleton()->get_setting("autoload/" + p_class->name); - if (autoload_path.begins_with("*")) { - // It's a singleton, and not just a regular AutoLoad script. - _set_error("The class \"" + p_class->name + "\" conflicts with the AutoLoad singleton of the same name, and is therefore redundant. Remove the class_name declaration to fix this error."); - } - return; + // Register locals. + switch (statement->type) { + case Node::VARIABLE: { + VariableNode *variable = static_cast<VariableNode *>(statement); + const SuiteNode::Local &local = current_suite->get_local(variable->identifier->name); + if (local.type != SuiteNode::Local::UNDEFINED) { + push_error(vformat(R"(There is already a %s named "%s" declared in this scope.)", local.get_name(), variable->identifier->name)); } - - tokenizer->advance(2); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - - if ((tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING)) { -#ifdef TOOLS_ENABLED - if (Engine::get_singleton()->is_editor_hint()) { - Variant constant = tokenizer->get_token_constant(); - String icon_path = constant.operator String(); - - String abs_icon_path = icon_path.is_rel_path() ? self_path.get_base_dir().plus_file(icon_path).simplify_path() : icon_path; - if (!FileAccess::exists(abs_icon_path)) { - _set_error("No class icon found at: " + abs_icon_path); - return; - } - - p_class->icon_path = icon_path; - } -#endif - - tokenizer->advance(); + current_suite->add_local(variable); + break; + } + case Node::CONSTANT: { + ConstantNode *constant = static_cast<ConstantNode *>(statement); + const SuiteNode::Local &local = current_suite->get_local(constant->identifier->name); + if (local.type != SuiteNode::Local::UNDEFINED) { + String name; + if (local.type == SuiteNode::Local::CONSTANT) { + name = "constant"; } else { - _set_error("The optional parameter after \"class_name\" must be a string constant file path to an icon."); - return; - } - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT) { - _set_error("The class icon must be separated by a comma."); - return; - } - - } break; - case GDScriptTokenizer::TK_PR_TOOL: { - if (p_class->tool) { - _set_error("The \"tool\" keyword can only be present once per script."); - return; - } - - p_class->tool = true; - tokenizer->advance(); - - } break; - case GDScriptTokenizer::TK_PR_CLASS: { - //class inside class :D - - StringName name; - - if (tokenizer->get_token(1) != GDScriptTokenizer::TK_IDENTIFIER) { - _set_error("\"class\" syntax: \"class <Name>:\" or \"class <Name> extends <BaseClass>:\""); - return; - } - name = tokenizer->get_token_identifier(1); - tokenizer->advance(2); - - // Check if name is shadowing something else - if (ClassDB::class_exists(name) || ClassDB::class_exists("_" + name.operator String())) { - _set_error("The class \"" + String(name) + "\" shadows a native class."); - return; - } - if (ScriptServer::is_global_class(name)) { - _set_error("Can't override name of the unique global class \"" + name + "\". It already exists at: " + ScriptServer::get_global_class_path(p_class->name)); - return; - } - ClassNode *outer_class = p_class; - while (outer_class) { - for (int i = 0; i < outer_class->subclasses.size(); i++) { - if (outer_class->subclasses[i]->name == name) { - _set_error("Another class named \"" + String(name) + "\" already exists in this scope (at line " + itos(outer_class->subclasses[i]->line) + ")."); - return; - } - } - if (outer_class->constant_expressions.has(name)) { - _set_error("A constant named \"" + String(name) + "\" already exists in the outer class scope (at line" + itos(outer_class->constant_expressions[name].expression->line) + ")."); - return; + name = "variable"; } - for (int i = 0; i < outer_class->variables.size(); i++) { - if (outer_class->variables[i].identifier == name) { - _set_error("A variable named \"" + String(name) + "\" already exists in the outer class scope (at line " + itos(outer_class->variables[i].line) + ")."); - return; - } - } - - outer_class = outer_class->owner; + push_error(vformat(R"(There is already a %s named "%s" declared in this scope.)", name, constant->identifier->name)); } - - ClassNode *newclass = alloc_node<ClassNode>(); - newclass->initializer = alloc_node<BlockNode>(); - newclass->initializer->parent_class = newclass; - newclass->ready = alloc_node<BlockNode>(); - newclass->ready->parent_class = newclass; - newclass->name = name; - newclass->owner = p_class; - - p_class->subclasses.push_back(newclass); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_EXTENDS) { - _parse_extends(newclass); - if (error_set) { - return; - } - } - - if (!_enter_indent_block()) { - _set_error("Indented block expected."); - return; - } - current_class = newclass; - _parse_class(newclass); - current_class = p_class; - - } break; - /* this is for functions.... - case GDScriptTokenizer::TK_CF_PASS: { - - tokenizer->advance(1); - } break; - */ - case GDScriptTokenizer::TK_PR_STATIC: { - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - _set_error("Expected \"func\"."); - return; - } - - [[fallthrough]]; + current_suite->add_local(constant); + break; } - case GDScriptTokenizer::TK_PR_FUNCTION: { - bool _static = false; - pending_newline = -1; - - if (tokenizer->get_token(-1) == GDScriptTokenizer::TK_PR_STATIC) { - _static = true; - } - - tokenizer->advance(); - StringName name; + default: + break; + } - if (_get_completable_identifier(COMPLETION_VIRTUAL_FUNC, name)) { - } + } while (multiline && !check(GDScriptTokenizer::Token::DEDENT) && !is_at_end()); - if (name == StringName()) { - _set_error("Expected an identifier after \"func\" (syntax: \"func <identifier>([arguments]):\")."); - return; - } + if (multiline) { + consume(GDScriptTokenizer::Token::DEDENT, vformat(R"(Missing unindent at the end of %s.)", p_context)); + } - for (int i = 0; i < p_class->functions.size(); i++) { - if (p_class->functions[i]->name == name) { - _set_error("The function \"" + String(name) + "\" already exists in this class (at line " + itos(p_class->functions[i]->line) + ")."); - } - } - for (int i = 0; i < p_class->static_functions.size(); i++) { - if (p_class->static_functions[i]->name == name) { - _set_error("The function \"" + String(name) + "\" already exists in this class (at line " + itos(p_class->static_functions[i]->line) + ")."); - } - } + current_suite = suite->parent_block; + return suite; +} +GDScriptParser::Node *GDScriptParser::parse_statement() { + Node *result = nullptr; #ifdef DEBUG_ENABLED - if (p_class->constant_expressions.has(name)) { - _add_warning(GDScriptWarning::FUNCTION_CONFLICTS_CONSTANT, -1, name); - } - for (int i = 0; i < p_class->variables.size(); i++) { - if (p_class->variables[i].identifier == name) { - _add_warning(GDScriptWarning::FUNCTION_CONFLICTS_VARIABLE, -1, name); - } - } - for (int i = 0; i < p_class->subclasses.size(); i++) { - if (p_class->subclasses[i]->name == name) { - _add_warning(GDScriptWarning::FUNCTION_CONFLICTS_CONSTANT, -1, name); - } - } -#endif // DEBUG_ENABLED + bool unreachable = current_suite->has_return && !current_suite->has_unreachable_code; +#endif - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected \"(\" after the identifier (syntax: \"func <identifier>([arguments]):\" )."); - return; + switch (current.type) { + case GDScriptTokenizer::Token::PASS: + advance(); + result = alloc_node<PassNode>(); + end_statement(R"("pass")"); + break; + case GDScriptTokenizer::Token::VAR: + advance(); + result = parse_variable(); + break; + case GDScriptTokenizer::Token::CONST: + advance(); + result = parse_constant(); + break; + case GDScriptTokenizer::Token::IF: + advance(); + result = parse_if(); + break; + case GDScriptTokenizer::Token::FOR: + advance(); + result = parse_for(); + break; + case GDScriptTokenizer::Token::WHILE: + advance(); + result = parse_while(); + break; + case GDScriptTokenizer::Token::MATCH: + advance(); + result = parse_match(); + break; + case GDScriptTokenizer::Token::BREAK: + advance(); + result = parse_break(); + break; + case GDScriptTokenizer::Token::CONTINUE: + advance(); + result = parse_continue(); + break; + case GDScriptTokenizer::Token::RETURN: { + advance(); + ReturnNode *n_return = alloc_node<ReturnNode>(); + if (!is_statement_end()) { + if (current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) { + push_error(R"(Constructor cannot return a value.)"); } + n_return->return_value = parse_expression(false); + } + result = n_return; - tokenizer->advance(); - - Vector<StringName> arguments; - Vector<DataType> argument_types; - Vector<Node *> default_values; -#ifdef DEBUG_ENABLED - Vector<int> arguments_usage; -#endif // DEBUG_ENABLED - - int fnline = tokenizer->get_token_line(); - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - //has arguments - bool defaulting = false; - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - continue; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_VAR) { - tokenizer->advance(); //var before the identifier is allowed - } + current_suite->has_return = true; - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Expected an identifier for an argument."); - return; - } + end_statement("return statement"); + break; + } + case GDScriptTokenizer::Token::BREAKPOINT: + advance(); + result = alloc_node<BreakpointNode>(); + end_statement(R"("breakpoint")"); + break; + case GDScriptTokenizer::Token::ASSERT: + advance(); + result = parse_assert(); + break; + case GDScriptTokenizer::Token::ANNOTATION: { + advance(); + AnnotationNode *annotation = parse_annotation(AnnotationInfo::STATEMENT); + if (annotation != nullptr) { + annotation_stack.push_back(annotation); + } + break; + } + default: { + // Expression statement. + ExpressionNode *expression = parse_expression(true); // Allow assignment here. + if (expression == nullptr) { + push_error(vformat(R"(Expected statement, found "%s" instead.)", previous.get_name())); + } + end_statement("expression"); + result = expression; - StringName argname = tokenizer->get_token_identifier(); - for (int i = 0; i < arguments.size(); i++) { - if (arguments[i] == argname) { - _set_error("The argument name \"" + String(argname) + "\" is defined multiple times."); - return; - } - } - arguments.push_back(argname); #ifdef DEBUG_ENABLED - arguments_usage.push_back(0); -#endif // DEBUG_ENABLED - - tokenizer->advance(); - - DataType argtype; - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - argtype.infer_type = true; - tokenizer->advance(); - } else if (!_parse_type(argtype)) { - _set_error("Expected a type for an argument."); - return; - } - } - argument_types.push_back(argtype); - - if (defaulting && tokenizer->get_token() != GDScriptTokenizer::TK_OP_ASSIGN) { - _set_error("Default parameter expected."); - return; - } - - //tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_ASSIGN) { - defaulting = true; - tokenizer->advance(1); - Node *defval = _parse_and_reduce_expression(p_class, _static); - if (!defval || error_set) { - return; - } - - OperatorNode *on = alloc_node<OperatorNode>(); - on->op = OperatorNode::OP_ASSIGN; - on->line = fnline; - - IdentifierNode *in = alloc_node<IdentifierNode>(); - in->name = argname; - in->line = fnline; - - on->arguments.push_back(in); - on->arguments.push_back(defval); - /* no .. - if (defval->type!=Node::TYPE_CONSTANT) { - - _set_error("default argument must be constant"); - } - */ - default_values.push_back(on); - } - - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - continue; - } else if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \",\" or \")\"."); - return; - } - + if (expression != nullptr) { + switch (expression->type) { + case Node::CALL: + case Node::ASSIGNMENT: + case Node::AWAIT: + // Fine. break; - } + default: + push_warning(expression, GDScriptWarning::STANDALONE_EXPRESSION); } + } +#endif + break; + } + } - tokenizer->advance(); - - BlockNode *block = alloc_node<BlockNode>(); - block->parent_class = p_class; - - FunctionNode *function = alloc_node<FunctionNode>(); - function->name = name; - function->arguments = arguments; - function->argument_types = argument_types; - function->default_values = default_values; - function->_static = _static; - function->line = fnline; #ifdef DEBUG_ENABLED - function->arguments_usage = arguments_usage; -#endif // DEBUG_ENABLED - function->rpc_mode = rpc_mode; - rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; - - if (name == "_init") { - if (_static) { - _set_error("The constructor cannot be static."); - return; - } - - if (p_class->extends_used) { - OperatorNode *cparent = alloc_node<OperatorNode>(); - cparent->op = OperatorNode::OP_PARENT_CALL; - block->statements.push_back(cparent); - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = "_init"; - cparent->arguments.push_back(id); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD) { - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected \"(\" for parent constructor arguments."); - return; - } - tokenizer->advance(); - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - //has arguments - parenthesis++; - while (true) { - current_function = function; - Node *arg = _parse_and_reduce_expression(p_class, _static); - if (!arg) { - return; - } - current_function = nullptr; - cparent->arguments.push_back(arg); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - continue; - } else if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \",\" or \")\"."); - return; - } - - break; - } - parenthesis--; - } - - tokenizer->advance(); - } - } else { - if (tokenizer->get_token() == GDScriptTokenizer::TK_PERIOD) { - _set_error("Parent constructor call found for a class without inheritance."); - return; - } - } - } - - DataType return_type; - if (tokenizer->get_token() == GDScriptTokenizer::TK_FORWARD_ARROW) { - if (!_parse_type(return_type, true)) { - _set_error("Expected a return type for the function."); - return; - } - } - - if (!_enter_indent_block(block)) { - _set_error(vformat("Indented block expected after declaration of \"%s\" function.", function->name)); - return; - } - - function->return_type = return_type; - - if (_static) { - p_class->static_functions.push_back(function); - } else { - p_class->functions.push_back(function); - } - - current_function = function; - function->body = block; - current_block = block; - _parse_block(block, _static); - current_block = nullptr; - - //arguments - } break; - case GDScriptTokenizer::TK_PR_SIGNAL: { - tokenizer->advance(); - - if (!tokenizer->is_token_literal()) { - _set_error("Expected an identifier after \"signal\"."); - return; - } - - ClassNode::Signal sig; - sig.name = tokenizer->get_token_identifier(); - sig.emissions = 0; - sig.line = tokenizer->get_token_line(); - - for (int i = 0; i < current_class->_signals.size(); i++) { - if (current_class->_signals[i].name == sig.name) { - _set_error("The signal \"" + sig.name + "\" already exists in this class (at line: " + itos(current_class->_signals[i].line) + ")."); - return; - } - } - - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - tokenizer->advance(); - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - continue; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - tokenizer->advance(); - break; - } - - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Expected an identifier in a \"signal\" argument."); - return; - } - - sig.arguments.push_back(tokenizer->get_token_identifier()); - tokenizer->advance(); + if (unreachable) { + current_suite->has_unreachable_code = true; + push_warning(result, GDScriptWarning::UNREACHABLE_CODE, current_function->identifier->name); + } +#endif - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } + if (panic_mode) { + synchronize(); + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - } else if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \",\" or \")\" after a \"signal\" parameter identifier."); - return; - } - } - } + return result; +} - p_class->_signals.push_back(sig); +GDScriptParser::AssertNode *GDScriptParser::parse_assert() { + // TODO: Add assert message. + AssertNode *assert = alloc_node<AssertNode>(); - if (!_end_statement()) { - _set_end_statement_error("signal"); - return; - } - } break; - case GDScriptTokenizer::TK_PR_EXPORT: { - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { -#define _ADVANCE_AND_CONSUME_NEWLINES \ - do { \ - tokenizer->advance(); \ - } while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) - - _ADVANCE_AND_CONSUME_NEWLINES; - parenthesis++; - - String hint_prefix = ""; - bool is_arrayed = false; - - while (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE && - tokenizer->get_token_type() == Variant::ARRAY && - tokenizer->get_token(1) == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); // Array - tokenizer->advance(); // Comma - if (is_arrayed) { - hint_prefix += itos(Variant::ARRAY) + ":"; - } else { - is_arrayed = true; - } - } + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "assert".)"); + assert->condition = parse_expression(false); + if (assert->condition == nullptr) { + push_error("Expected expression to assert."); + return nullptr; + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE) { - Variant::Type type = tokenizer->get_token_type(); - if (type == Variant::NIL) { - _set_error("Can't export null type."); - return; - } - if (type == Variant::OBJECT) { - _set_error("Can't export raw object type."); - return; - } - current_export.type = type; - current_export.usage |= PROPERTY_USAGE_SCRIPT_VARIABLE; - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - // hint expected next! - _ADVANCE_AND_CONSUME_NEWLINES; - - switch (type) { - case Variant::INT: { - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "FLAGS") { - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - _set_error("Expected \",\" in the bit flags hint."); - return; - } - - current_export.hint = PROPERTY_HINT_FLAGS; - _ADVANCE_AND_CONSUME_NEWLINES; - - bool first = true; - while (true) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || tokenizer->get_token_constant().get_type() != Variant::STRING) { - current_export = PropertyInfo(); - _set_error("Expected a string constant in the named bit flags hint."); - return; - } - - String c = tokenizer->get_token_constant(); - if (!first) { - current_export.hint_string += ","; - } else { - first = false; - } - - current_export.hint_string += c.xml_escape(); - - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - current_export = PropertyInfo(); - _set_error("Expected \")\" or \",\" in the named bit flags hint."); - return; - } - _ADVANCE_AND_CONSUME_NEWLINES; - } - - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "LAYERS_2D_RENDER") { - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the layers 2D render hint."); - return; - } - current_export.hint = PROPERTY_HINT_LAYERS_2D_RENDER; - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "LAYERS_2D_PHYSICS") { - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the layers 2D physics hint."); - return; - } - current_export.hint = PROPERTY_HINT_LAYERS_2D_PHYSICS; - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "LAYERS_3D_RENDER") { - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the layers 3D render hint."); - return; - } - current_export.hint = PROPERTY_HINT_LAYERS_3D_RENDER; - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "LAYERS_3D_PHYSICS") { - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the layers 3D physics hint."); - return; - } - current_export.hint = PROPERTY_HINT_LAYERS_3D_PHYSICS; - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING) { - //enumeration - current_export.hint = PROPERTY_HINT_ENUM; - bool first = true; - while (true) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || tokenizer->get_token_constant().get_type() != Variant::STRING) { - current_export = PropertyInfo(); - _set_error("Expected a string constant in the enumeration hint."); - return; - } - - String c = tokenizer->get_token_constant(); - if (!first) { - current_export.hint_string += ","; - } else { - first = false; - } - - current_export.hint_string += c.xml_escape(); - - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - current_export = PropertyInfo(); - _set_error("Expected \")\" or \",\" in the enumeration hint."); - return; - } - - _ADVANCE_AND_CONSUME_NEWLINES; - } - - break; - } - - [[fallthrough]]; - } - case Variant::FLOAT: { - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "EASE") { - current_export.hint = PROPERTY_HINT_EXP_EASING; - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the hint."); - return; - } - break; - } - - // range - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "EXP") { - current_export.hint = PROPERTY_HINT_EXP_RANGE; - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } else if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - _set_error("Expected \")\" or \",\" in the exponential range hint."); - return; - } - _ADVANCE_AND_CONSUME_NEWLINES; - } else { - current_export.hint = PROPERTY_HINT_RANGE; - } - - float sign = 1.0; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_SUB) { - sign = -1; - _ADVANCE_AND_CONSUME_NEWLINES; - } - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || !tokenizer->get_token_constant().is_num()) { - current_export = PropertyInfo(); - _set_error("Expected a range in the numeric hint."); - return; - } - - current_export.hint_string = rtos(sign * double(tokenizer->get_token_constant())); - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - current_export.hint_string = "0," + current_export.hint_string; - break; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - current_export = PropertyInfo(); - _set_error("Expected \",\" or \")\" in the numeric range hint."); - return; - } - - _ADVANCE_AND_CONSUME_NEWLINES; - - sign = 1.0; - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_SUB) { - sign = -1; - _ADVANCE_AND_CONSUME_NEWLINES; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || !tokenizer->get_token_constant().is_num()) { - current_export = PropertyInfo(); - _set_error("Expected a number as upper bound in the numeric range hint."); - return; - } - - current_export.hint_string += "," + rtos(sign * double(tokenizer->get_token_constant())); - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - current_export = PropertyInfo(); - _set_error("Expected \",\" or \")\" in the numeric range hint."); - return; - } - - _ADVANCE_AND_CONSUME_NEWLINES; - sign = 1.0; - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_SUB) { - sign = -1; - _ADVANCE_AND_CONSUME_NEWLINES; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || !tokenizer->get_token_constant().is_num()) { - current_export = PropertyInfo(); - _set_error("Expected a number as step in the numeric range hint."); - return; - } - - current_export.hint_string += "," + rtos(sign * double(tokenizer->get_token_constant())); - _ADVANCE_AND_CONSUME_NEWLINES; - - } break; - case Variant::STRING: { - if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING) { - //enumeration - current_export.hint = PROPERTY_HINT_ENUM; - bool first = true; - while (true) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || tokenizer->get_token_constant().get_type() != Variant::STRING) { - current_export = PropertyInfo(); - _set_error("Expected a string constant in the enumeration hint."); - return; - } - - String c = tokenizer->get_token_constant(); - if (!first) { - current_export.hint_string += ","; - } else { - first = false; - } - - current_export.hint_string += c.xml_escape(); - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - current_export = PropertyInfo(); - _set_error("Expected \")\" or \",\" in the enumeration hint."); - return; - } - _ADVANCE_AND_CONSUME_NEWLINES; - } - - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "DIR") { - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - current_export.hint = PROPERTY_HINT_DIR; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() != GDScriptTokenizer::TK_IDENTIFIER || !(tokenizer->get_token_identifier() == "GLOBAL")) { - _set_error("Expected \"GLOBAL\" after comma in the directory hint."); - return; - } - if (!p_class->tool) { - _set_error("Global filesystem hints may only be used in tool scripts."); - return; - } - current_export.hint = PROPERTY_HINT_GLOBAL_DIR; - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the hint."); - return; - } - } else { - _set_error("Expected \")\" or \",\" in the hint."); - return; - } - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "FILE") { - current_export.hint = PROPERTY_HINT_FILE; - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "GLOBAL") { - if (!p_class->tool) { - _set_error("Global filesystem hints may only be used in tool scripts."); - return; - } - current_export.hint = PROPERTY_HINT_GLOBAL_FILE; - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - break; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - _ADVANCE_AND_CONSUME_NEWLINES; - } else { - _set_error("Expected \")\" or \",\" in the hint."); - return; - } - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_CONSTANT || tokenizer->get_token_constant().get_type() != Variant::STRING) { - if (current_export.hint == PROPERTY_HINT_GLOBAL_FILE) { - _set_error("Expected string constant with filter."); - } else { - _set_error("Expected \"GLOBAL\" or string constant with filter."); - } - return; - } - current_export.hint_string = tokenizer->get_token_constant(); - _ADVANCE_AND_CONSUME_NEWLINES; - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the hint."); - return; - } - break; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "MULTILINE") { - current_export.hint = PROPERTY_HINT_MULTILINE_TEXT; - _ADVANCE_AND_CONSUME_NEWLINES; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" in the hint."); - return; - } - break; - } - } break; - case Variant::COLOR: { - if (tokenizer->get_token() != GDScriptTokenizer::TK_IDENTIFIER) { - current_export = PropertyInfo(); - _set_error("Color type hint expects RGB or RGBA as hints."); - return; - } - - String identifier = tokenizer->get_token_identifier(); - if (identifier == "RGB") { - current_export.hint = PROPERTY_HINT_COLOR_NO_ALPHA; - } else if (identifier == "RGBA") { - //none - } else { - current_export = PropertyInfo(); - _set_error("Color type hint expects RGB or RGBA as hints."); - return; - } - _ADVANCE_AND_CONSUME_NEWLINES; - - } break; - default: { - current_export = PropertyInfo(); - _set_error("Type \"" + Variant::get_type_name(type) + "\" can't take hints."); - return; - } break; - } - } + if (match(GDScriptTokenizer::Token::COMMA)) { + // Error message. + if (consume(GDScriptTokenizer::Token::LITERAL, R"(Expected error message for assert after ",".)")) { + assert->message = parse_literal(); + if (assert->message->value.get_type() != Variant::STRING) { + push_error(R"(Expected string for assert error message.)"); + } + } else { + return nullptr; + } + } - } else { - parenthesis++; - Node *subexpr = _parse_and_reduce_expression(p_class, true, true); - if (!subexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - parenthesis--; + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after assert expression.)*"); - if (subexpr->type != Node::TYPE_CONSTANT) { - current_export = PropertyInfo(); - _set_error("Expected a constant expression."); - } + end_statement(R"("assert")"); - Variant constant = static_cast<ConstantNode *>(subexpr)->value; + return assert; +} - if (constant.get_type() == Variant::OBJECT) { - GDScriptNativeClass *native_class = Object::cast_to<GDScriptNativeClass>(constant); +GDScriptParser::BreakNode *GDScriptParser::parse_break() { + if (!can_break) { + push_error(R"(Cannot use "break" outside of a loop.)"); + } + end_statement(R"("break")"); + return alloc_node<BreakNode>(); +} - if (native_class && ClassDB::is_parent_class(native_class->get_name(), "Resource")) { - current_export.type = Variant::OBJECT; - current_export.hint = PROPERTY_HINT_RESOURCE_TYPE; - current_export.usage |= PROPERTY_USAGE_SCRIPT_VARIABLE; +GDScriptParser::ContinueNode *GDScriptParser::parse_continue() { + if (!can_continue) { + push_error(R"(Cannot use "continue" outside of a loop or pattern matching block.)"); + } + current_suite->has_continue = true; + end_statement(R"("continue")"); + return alloc_node<ContinueNode>(); +} - current_export.hint_string = native_class->get_name(); - current_export.class_name = native_class->get_name(); +GDScriptParser::ForNode *GDScriptParser::parse_for() { + ForNode *n_for = alloc_node<ForNode>(); - } else { - current_export = PropertyInfo(); - _set_error("The export hint isn't a resource type."); - } - } else if (constant.get_type() == Variant::DICTIONARY) { - // Enumeration - bool is_flags = false; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - _ADVANCE_AND_CONSUME_NEWLINES; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER && tokenizer->get_token_identifier() == "FLAGS") { - is_flags = true; - _ADVANCE_AND_CONSUME_NEWLINES; - } else { - current_export = PropertyInfo(); - _set_error("Expected \"FLAGS\" after comma."); - } - } + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected loop variable name after "for".)")) { + n_for->variable = parse_identifier(); + } - current_export.type = Variant::INT; - current_export.hint = is_flags ? PROPERTY_HINT_FLAGS : PROPERTY_HINT_ENUM; - current_export.usage |= PROPERTY_USAGE_SCRIPT_VARIABLE; - Dictionary enum_values = constant; - - List<Variant> keys; - enum_values.get_key_list(&keys); - - bool first = true; - for (List<Variant>::Element *E = keys.front(); E; E = E->next()) { - if (enum_values[E->get()].get_type() == Variant::INT) { - if (!first) { - current_export.hint_string += ","; - } else { - first = false; - } - - current_export.hint_string += E->get().operator String().camelcase_to_underscore(true).capitalize().xml_escape(); - if (!is_flags) { - current_export.hint_string += ":"; - current_export.hint_string += enum_values[E->get()].operator String().xml_escape(); - } - } - } - } else { - current_export = PropertyInfo(); - _set_error("Expected type for export."); - return; - } - } + consume(GDScriptTokenizer::Token::IN, R"(Expected "in" after "for" variable name.)"); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - current_export = PropertyInfo(); - _set_error("Expected \")\" or \",\" after the export hint."); - return; - } + n_for->list = parse_expression(false); - tokenizer->advance(); - parenthesis--; + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "for" condition.)"); - if (is_arrayed) { - hint_prefix += itos(current_export.type); - if (current_export.hint) { - hint_prefix += "/" + itos(current_export.hint); - } - current_export.hint_string = hint_prefix + ":" + current_export.hint_string; - current_export.hint = PROPERTY_HINT_TYPE_STRING; - current_export.type = Variant::ARRAY; - } -#undef _ADVANCE_AND_CONSUME_NEWLINES - } + // Save break/continue state. + bool could_break = can_break; + bool could_continue = can_continue; - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_ONREADY && tokenizer->get_token() != GDScriptTokenizer::TK_PR_REMOTE && tokenizer->get_token() != GDScriptTokenizer::TK_PR_MASTER && tokenizer->get_token() != GDScriptTokenizer::TK_PR_PUPPET && tokenizer->get_token() != GDScriptTokenizer::TK_PR_REMOTESYNC && tokenizer->get_token() != GDScriptTokenizer::TK_PR_MASTERSYNC && tokenizer->get_token() != GDScriptTokenizer::TK_PR_PUPPETSYNC) { - current_export = PropertyInfo(); - _set_error("Expected \"var\", \"onready\", \"remote\", \"master\", \"puppet\", \"remotesync\", \"mastersync\", \"puppetsync\"."); - return; - } + // Allow break/continue. + can_break = true; + can_continue = true; - continue; - } break; - case GDScriptTokenizer::TK_PR_ONREADY: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR) { - _set_error("Expected \"var\"."); - return; - } + SuiteNode *suite = alloc_node<SuiteNode>(); + if (n_for->variable) { + suite->add_local(SuiteNode::Local(n_for->variable)); + } + suite->parent_for = n_for; - continue; - } break; - case GDScriptTokenizer::TK_PR_REMOTE: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (current_export.type) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR) { - _set_error("Expected \"var\"."); - return; - } + n_for->loop = parse_suite(R"("for" block)", suite); - } else { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - _set_error("Expected \"var\" or \"func\"."); - return; - } - } - rpc_mode = MultiplayerAPI::RPC_MODE_REMOTE; - - continue; - } break; - case GDScriptTokenizer::TK_PR_MASTER: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (current_export.type) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR) { - _set_error("Expected \"var\"."); - return; - } + // Reset break/continue state. + can_break = could_break; + can_continue = could_continue; - } else { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - _set_error("Expected \"var\" or \"func\"."); - return; - } - } + return n_for; +} - rpc_mode = MultiplayerAPI::RPC_MODE_MASTER; - continue; - } break; - case GDScriptTokenizer::TK_PR_PUPPET: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (current_export.type) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR) { - _set_error("Expected \"var\"."); - return; - } +GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { + IfNode *n_if = alloc_node<IfNode>(); - } else { - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - _set_error("Expected \"var\" or \"func\"."); - return; - } - } + n_if->condition = parse_expression(false); + if (n_if->condition == nullptr) { + push_error(vformat(R"(Expected conditional expression after "%s".)", p_token)); + } - rpc_mode = MultiplayerAPI::RPC_MODE_PUPPET; - continue; - } break; - case GDScriptTokenizer::TK_PR_REMOTESYNC: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - if (current_export.type) { - _set_error("Expected \"var\"."); - } else { - _set_error("Expected \"var\" or \"func\"."); - } - return; - } + consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after "%s" condition.)", p_token)); - rpc_mode = MultiplayerAPI::RPC_MODE_REMOTESYNC; - continue; - } break; - case GDScriptTokenizer::TK_PR_MASTERSYNC: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - if (current_export.type) { - _set_error("Expected \"var\"."); - } else { - _set_error("Expected \"var\" or \"func\"."); - } - return; - } + n_if->true_block = parse_suite(vformat(R"("%s" block)", p_token)); + n_if->true_block->parent_if = n_if; - rpc_mode = MultiplayerAPI::RPC_MODE_MASTERSYNC; - continue; - } break; - case GDScriptTokenizer::TK_PR_PUPPETSYNC: { - //may be fallthrough from export, ignore if so - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PR_VAR && tokenizer->get_token() != GDScriptTokenizer::TK_PR_FUNCTION) { - if (current_export.type) { - _set_error("Expected \"var\"."); - } else { - _set_error("Expected \"var\" or \"func\"."); - } - return; - } + if (n_if->true_block->has_continue) { + current_suite->has_continue = true; + } - rpc_mode = MultiplayerAPI::RPC_MODE_PUPPETSYNC; - continue; - } break; - case GDScriptTokenizer::TK_PR_VAR: { - // variable declaration and (eventual) initialization + if (match(GDScriptTokenizer::Token::ELIF)) { + IfNode *elif = parse_if("elif"); - ClassNode::Member member; + SuiteNode *else_block = alloc_node<SuiteNode>(); + else_block->statements.push_back(elif); + n_if->false_block = else_block; + } else if (match(GDScriptTokenizer::Token::ELSE)) { + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "else".)"); + n_if->false_block = parse_suite(R"("else" block)"); + } - bool autoexport = tokenizer->get_token(-1) == GDScriptTokenizer::TK_PR_EXPORT; - if (current_export.type != Variant::NIL) { - member._export = current_export; - current_export = PropertyInfo(); - } + if (n_if->false_block != nullptr && n_if->false_block->has_return && n_if->true_block->has_return) { + current_suite->has_return = true; + } + if (n_if->false_block != nullptr && n_if->false_block->has_continue) { + current_suite->has_continue = true; + } - bool onready = tokenizer->get_token(-1) == GDScriptTokenizer::TK_PR_ONREADY; + return n_if; +} - tokenizer->advance(); - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Expected an identifier for the member variable name."); - return; - } +GDScriptParser::MatchNode *GDScriptParser::parse_match() { + MatchNode *match = alloc_node<MatchNode>(); - member.identifier = tokenizer->get_token_literal(); - member.expression = nullptr; - member._export.name = member.identifier; - member.line = tokenizer->get_token_line(); - member.usages = 0; - member.rpc_mode = rpc_mode; - - if (current_class->constant_expressions.has(member.identifier)) { - _set_error("A constant named \"" + String(member.identifier) + "\" already exists in this class (at line: " + - itos(current_class->constant_expressions[member.identifier].expression->line) + ")."); - return; - } + match->test = parse_expression(false); + if (match->test == nullptr) { + push_error(R"(Expected expression to test after "match".)"); + } - for (int i = 0; i < current_class->variables.size(); i++) { - if (current_class->variables[i].identifier == member.identifier) { - _set_error("Variable \"" + String(member.identifier) + "\" already exists in this class (at line: " + - itos(current_class->variables[i].line) + ")."); - return; - } - } + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "match" expression.)"); + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected a newline after "match" statement.)"); - for (int i = 0; i < current_class->subclasses.size(); i++) { - if (current_class->subclasses[i]->name == member.identifier) { - _set_error("A class named \"" + String(member.identifier) + "\" already exists in this class (at line " + itos(current_class->subclasses[i]->line) + ")."); - return; - } - } -#ifdef DEBUG_ENABLED - for (int i = 0; i < current_class->functions.size(); i++) { - if (current_class->functions[i]->name == member.identifier) { - _add_warning(GDScriptWarning::VARIABLE_CONFLICTS_FUNCTION, member.line, member.identifier); - break; - } - } - for (int i = 0; i < current_class->static_functions.size(); i++) { - if (current_class->static_functions[i]->name == member.identifier) { - _add_warning(GDScriptWarning::VARIABLE_CONFLICTS_FUNCTION, member.line, member.identifier); - break; - } - } -#endif // DEBUG_ENABLED - tokenizer->advance(); - - rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; + if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected an indented block after "match" statement.)")) { + return match; + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - member.data_type = DataType(); #ifdef DEBUG_ENABLED - member.data_type.infer_type = true; + bool all_have_return = true; + bool have_wildcard = false; + bool wildcard_has_return = false; + bool have_wildcard_without_continue = false; #endif - tokenizer->advance(); - } else if (!_parse_type(member.data_type)) { - _set_error("Expected a type for the class variable."); - return; - } - } - - if (autoexport && member.data_type.has_type) { - if (member.data_type.kind == DataType::BUILTIN) { - member._export.type = member.data_type.builtin_type; - } else if (member.data_type.kind == DataType::NATIVE) { - if (ClassDB::is_parent_class(member.data_type.native_type, "Resource")) { - member._export.type = Variant::OBJECT; - member._export.hint = PROPERTY_HINT_RESOURCE_TYPE; - member._export.usage |= PROPERTY_USAGE_SCRIPT_VARIABLE; - member._export.hint_string = member.data_type.native_type; - member._export.class_name = member.data_type.native_type; - } else { - _set_error("Invalid export type. Only built-in and native resource types can be exported.", member.line); - return; - } - - } else { - _set_error("Invalid export type. Only built-in and native resource types can be exported.", member.line); - return; - } - } -#ifdef TOOLS_ENABLED - Callable::CallError ce; - member.default_value = Variant::construct(member._export.type, nullptr, 0, ce); -#endif + while (!check(GDScriptTokenizer::Token::DEDENT) && !is_at_end()) { + MatchBranchNode *branch = parse_match_branch(); + if (branch == nullptr) { + continue; + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_ASSIGN) { #ifdef DEBUG_ENABLED - int line = tokenizer->get_token_line(); -#endif - tokenizer->advance(); - - Node *subexpr = _parse_and_reduce_expression(p_class, false, autoexport || member._export.type != Variant::NIL); - if (!subexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - - //discourage common error - if (!onready && subexpr->type == Node::TYPE_OPERATOR) { - OperatorNode *op = static_cast<OperatorNode *>(subexpr); - if (op->op == OperatorNode::OP_CALL && op->arguments[0]->type == Node::TYPE_SELF && op->arguments[1]->type == Node::TYPE_IDENTIFIER) { - IdentifierNode *id = static_cast<IdentifierNode *>(op->arguments[1]); - if (id->name == "get_node") { - _set_error("Use \"onready var " + String(member.identifier) + " = get_node(...)\" instead."); - return; - } - } - } - - member.expression = subexpr; - - if (autoexport && !member.data_type.has_type) { - if (subexpr->type != Node::TYPE_CONSTANT) { - _set_error("Type-less export needs a constant expression assigned to infer type."); - return; - } - - ConstantNode *cn = static_cast<ConstantNode *>(subexpr); - if (cn->value.get_type() == Variant::NIL) { - _set_error("Can't accept a null constant expression for inferring export type."); - return; - } - - if (!_reduce_export_var_type(cn->value, member.line)) { - return; - } - - member._export.type = cn->value.get_type(); - member._export.usage |= PROPERTY_USAGE_SCRIPT_VARIABLE; - if (cn->value.get_type() == Variant::OBJECT) { - Object *obj = cn->value; - Resource *res = Object::cast_to<Resource>(obj); - if (res == nullptr) { - _set_error("The exported constant isn't a type or resource."); - return; - } - member._export.hint = PROPERTY_HINT_RESOURCE_TYPE; - member._export.hint_string = res->get_class(); - } - } -#ifdef TOOLS_ENABLED - if (subexpr->type == Node::TYPE_CONSTANT && (member._export.type != Variant::NIL || member.data_type.has_type)) { - ConstantNode *cn = static_cast<ConstantNode *>(subexpr); - if (cn->value.get_type() != Variant::NIL) { - if (member._export.type != Variant::NIL && cn->value.get_type() != member._export.type) { - if (Variant::can_convert(cn->value.get_type(), member._export.type)) { - Callable::CallError err; - const Variant *args = &cn->value; - cn->value = Variant::construct(member._export.type, &args, 1, err); - } else { - _set_error("Can't convert the provided value to the export type."); - return; - } - } - member.default_value = cn->value; - } - } -#endif - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = member.identifier; - id->datatype = member.data_type; - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_INIT_ASSIGN; - op->arguments.push_back(id); - op->arguments.push_back(subexpr); + if (have_wildcard_without_continue) { + push_warning(branch->patterns[0], GDScriptWarning::UNREACHABLE_PATTERN); + } -#ifdef DEBUG_ENABLED - NewLineNode *nl2 = alloc_node<NewLineNode>(); - nl2->line = line; - if (onready) { - p_class->ready->statements.push_back(nl2); - } else { - p_class->initializer->statements.push_back(nl2); - } + if (branch->has_wildcard) { + have_wildcard = true; + if (branch->block->has_return) { + wildcard_has_return = true; + } + if (!branch->block->has_continue) { + have_wildcard_without_continue = true; + } + } + if (!branch->block->has_return) { + all_have_return = false; + } #endif - if (onready) { - p_class->ready->statements.push_back(op); - } else { - p_class->initializer->statements.push_back(op); - } - - member.initial_assignment = op; - - } else { - if (autoexport && !member.data_type.has_type) { - _set_error("Type-less export needs a constant expression assigned to infer type."); - return; - } - - Node *expr; - - if (member.data_type.has_type) { - expr = _get_default_value_for_type(member.data_type); - } else { - DataType exported_type; - exported_type.has_type = true; - exported_type.kind = DataType::BUILTIN; - exported_type.builtin_type = member._export.type; - expr = _get_default_value_for_type(exported_type); - } - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = member.identifier; - id->datatype = member.data_type; - - OperatorNode *op = alloc_node<OperatorNode>(); - op->op = OperatorNode::OP_INIT_ASSIGN; - op->arguments.push_back(id); - op->arguments.push_back(expr); - - p_class->initializer->statements.push_back(op); - - member.initial_assignment = op; - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_SETGET) { - tokenizer->advance(); - - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - //just comma means using only getter - if (!tokenizer->is_token_literal()) { - _set_error("Expected an identifier for the setter function after \"setget\"."); - } - - member.setter = tokenizer->get_token_literal(); - - tokenizer->advance(); - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - //there is a getter - tokenizer->advance(); - - if (!tokenizer->is_token_literal()) { - _set_error("Expected an identifier for the getter function after \",\"."); - } - - member.getter = tokenizer->get_token_literal(); - tokenizer->advance(); - } - } - - p_class->variables.push_back(member); - - if (!_end_statement()) { - _set_end_statement_error("var"); - return; - } - } break; - case GDScriptTokenizer::TK_PR_CONST: { - // constant declaration and initialization - - ClassNode::Constant constant; - - tokenizer->advance(); - if (!tokenizer->is_token_literal(0, true)) { - _set_error("Expected an identifier for the constant."); - return; - } - - StringName const_id = tokenizer->get_token_literal(); - int line = tokenizer->get_token_line(); - - if (current_class->constant_expressions.has(const_id)) { - _set_error("Constant \"" + String(const_id) + "\" already exists in this class (at line " + - itos(current_class->constant_expressions[const_id].expression->line) + ")."); - return; - } - - for (int i = 0; i < current_class->variables.size(); i++) { - if (current_class->variables[i].identifier == const_id) { - _set_error("A variable named \"" + String(const_id) + "\" already exists in this class (at line " + - itos(current_class->variables[i].line) + ")."); - return; - } - } - - for (int i = 0; i < current_class->subclasses.size(); i++) { - if (current_class->subclasses[i]->name == const_id) { - _set_error("A class named \"" + String(const_id) + "\" already exists in this class (at line " + itos(current_class->subclasses[i]->line) + ")."); - return; - } - } + match->branches.push_back(branch); + } - tokenizer->advance(); + consume(GDScriptTokenizer::Token::DEDENT, R"(Expected an indented block after "match" statement.)"); - if (tokenizer->get_token() == GDScriptTokenizer::TK_COLON) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_OP_ASSIGN) { - constant.type = DataType(); #ifdef DEBUG_ENABLED - constant.type.infer_type = true; + if (wildcard_has_return || (all_have_return && have_wildcard)) { + current_suite->has_return = true; + } #endif - tokenizer->advance(); - } else if (!_parse_type(constant.type)) { - _set_error("Expected a type for the class constant."); - return; - } - } - - if (tokenizer->get_token() != GDScriptTokenizer::TK_OP_ASSIGN) { - _set_error("Constants must be assigned immediately."); - return; - } - - tokenizer->advance(); - - Node *subexpr = _parse_and_reduce_expression(p_class, true, true); - if (!subexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - - if (subexpr->type != Node::TYPE_CONSTANT) { - _set_error("Expected a constant expression.", line); - return; - } - subexpr->line = line; - constant.expression = subexpr; - - p_class->constant_expressions.insert(const_id, constant); - - if (!_end_statement()) { - _set_end_statement_error("const"); - return; - } - - } break; - case GDScriptTokenizer::TK_PR_ENUM: { - //multiple constant declarations.. - - int last_assign = -1; // Incremented by 1 right before the assignment. - String enum_name; - Dictionary enum_dict; - - tokenizer->advance(); - if (tokenizer->is_token_literal(0, true)) { - enum_name = tokenizer->get_token_literal(); - - if (current_class->constant_expressions.has(enum_name)) { - _set_error("A constant named \"" + String(enum_name) + "\" already exists in this class (at line " + - itos(current_class->constant_expressions[enum_name].expression->line) + ")."); - return; - } - - for (int i = 0; i < current_class->variables.size(); i++) { - if (current_class->variables[i].identifier == enum_name) { - _set_error("A variable named \"" + String(enum_name) + "\" already exists in this class (at line " + - itos(current_class->variables[i].line) + ")."); - return; - } - } - - for (int i = 0; i < current_class->subclasses.size(); i++) { - if (current_class->subclasses[i]->name == enum_name) { - _set_error("A class named \"" + String(enum_name) + "\" already exists in this class (at line " + itos(current_class->subclasses[i]->line) + ")."); - return; - } - } - - tokenizer->advance(); - } - if (tokenizer->get_token() != GDScriptTokenizer::TK_CURLY_BRACKET_OPEN) { - _set_error("Expected \"{\" in the enum declaration."); - return; - } - tokenizer->advance(); - - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); // Ignore newlines - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURLY_BRACKET_CLOSE) { - tokenizer->advance(); - break; // End of enum - } else if (!tokenizer->is_token_literal(0, true)) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - _set_error("Unexpected end of file."); - } else { - _set_error(String("Unexpected ") + GDScriptTokenizer::get_token_name(tokenizer->get_token()) + ", expected an identifier."); - } - - return; - } else { // tokenizer->is_token_literal(0, true) - StringName const_id = tokenizer->get_token_literal(); - - tokenizer->advance(); - - ConstantNode *enum_value_expr; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_OP_ASSIGN) { - tokenizer->advance(); - - Node *subexpr = _parse_and_reduce_expression(p_class, true, true); - if (!subexpr) { - if (_recover_from_completion()) { - break; - } - return; - } - - if (subexpr->type != Node::TYPE_CONSTANT) { - _set_error("Expected a constant expression."); - return; - } - enum_value_expr = static_cast<ConstantNode *>(subexpr); - - if (enum_value_expr->value.get_type() != Variant::INT) { - _set_error("Expected an integer value for \"enum\"."); - return; - } - - last_assign = enum_value_expr->value; - - } else { - last_assign = last_assign + 1; - enum_value_expr = alloc_node<ConstantNode>(); - enum_value_expr->value = last_assign; - enum_value_expr->datatype = _type_from_variant(enum_value_expr->value); - } - - if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - tokenizer->advance(); - } else if (tokenizer->is_token_literal(0, true)) { - _set_error("Unexpected identifier."); - return; - } - - if (enum_name != "") { - enum_dict[const_id] = enum_value_expr->value; - } else { - if (current_class->constant_expressions.has(const_id)) { - _set_error("A constant named \"" + String(const_id) + "\" already exists in this class (at line " + - itos(current_class->constant_expressions[const_id].expression->line) + ")."); - return; - } - - for (int i = 0; i < current_class->variables.size(); i++) { - if (current_class->variables[i].identifier == const_id) { - _set_error("A variable named \"" + String(const_id) + "\" already exists in this class (at line " + - itos(current_class->variables[i].line) + ")."); - return; - } - } - - for (int i = 0; i < current_class->subclasses.size(); i++) { - if (current_class->subclasses[i]->name == const_id) { - _set_error("A class named \"" + String(const_id) + "\" already exists in this class (at line " + itos(current_class->subclasses[i]->line) + ")."); - return; - } - } - - ClassNode::Constant constant; - constant.type.has_type = true; - constant.type.kind = DataType::BUILTIN; - constant.type.builtin_type = Variant::INT; - constant.expression = enum_value_expr; - p_class->constant_expressions.insert(const_id, constant); - } - } - } + return match; +} - if (enum_name != "") { - ClassNode::Constant enum_constant; - ConstantNode *cn = alloc_node<ConstantNode>(); - cn->value = enum_dict; - cn->datatype = _type_from_variant(cn->value); +GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { + MatchBranchNode *branch = alloc_node<MatchBranchNode>(); - enum_constant.expression = cn; - enum_constant.type = cn->datatype; - p_class->constant_expressions.insert(enum_name, enum_constant); - } + bool has_bind = false; - if (!_end_statement()) { - _set_end_statement_error("enum"); - return; - } + do { + PatternNode *pattern = parse_match_pattern(); + if (pattern == nullptr) { + continue; + } + if (pattern->pattern_type == PatternNode::PT_BIND) { + has_bind = true; + } + if (branch->patterns.size() > 0 && has_bind) { + push_error(R"(Cannot use a variable bind with multiple patterns.)"); + } + if (pattern->pattern_type == PatternNode::PT_REST) { + push_error(R"(Rest pattern can only be used inside array and dictionary patterns.)"); + } else if (pattern->pattern_type == PatternNode::PT_BIND || pattern->pattern_type == PatternNode::PT_WILDCARD) { + branch->has_wildcard = true; + } + branch->patterns.push_back(pattern); + } while (match(GDScriptTokenizer::Token::COMMA)); - } break; + if (branch->patterns.empty()) { + push_error(R"(No pattern found for "match" branch.)"); + } - case GDScriptTokenizer::TK_CONSTANT: { - if (tokenizer->get_token_constant().get_type() == Variant::STRING) { - tokenizer->advance(); - // Ignore - } else { - _set_error(String() + "Unexpected constant of type: " + Variant::get_type_name(tokenizer->get_token_constant().get_type())); - return; - } - } break; + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "match" patterns.)"); - case GDScriptTokenizer::TK_CF_PASS: { - tokenizer->advance(); - } break; + // Save continue state. + bool could_continue = can_continue; + // Allow continue for match. + can_continue = true; - default: { - _set_error(String() + "Unexpected token: " + tokenizer->get_token_name(tokenizer->get_token()) + ":" + tokenizer->get_token_identifier()); - return; + SuiteNode *suite = alloc_node<SuiteNode>(); + if (branch->patterns.size() > 0) { + List<StringName> binds; + branch->patterns[0]->binds.get_key_list(&binds); - } break; + for (List<StringName>::Element *E = binds.front(); E != nullptr; E = E->next()) { + SuiteNode::Local local(branch->patterns[0]->binds[E->get()]); + suite->add_local(local); } } -} -void GDScriptParser::_determine_inheritance(ClassNode *p_class, bool p_recursive) { - if (p_class->base_type.has_type) { - // Already determined - } else if (p_class->extends_used) { - //do inheritance - String path = p_class->extends_file; + branch->block = parse_suite("match pattern block", suite); - Ref<GDScript> script; - StringName native; - ClassNode *base_class = nullptr; + // Restore continue state. + can_continue = could_continue; - if (path != "") { - //path (and optionally subclasses) + return branch; +} - if (path.is_rel_path()) { - String base = base_path; +GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_root_pattern) { + PatternNode *pattern = alloc_node<PatternNode>(); - if (base == "" || base.is_rel_path()) { - _set_error("Couldn't resolve relative path for the parent class: " + path, p_class->line); - return; - } - path = base.plus_file(path).simplify_path(); - } - script = ResourceLoader::load(path); - if (script.is_null()) { - _set_error("Couldn't load the base class: " + path, p_class->line); - return; + switch (current.type) { + case GDScriptTokenizer::Token::LITERAL: + advance(); + pattern->pattern_type = PatternNode::PT_LITERAL; + pattern->literal = parse_literal(); + if (pattern->literal == nullptr) { + // Error happened. + return nullptr; } - if (!script->is_valid()) { - _set_error("Script isn't fully loaded (cyclic preload?): " + path, p_class->line); - return; + break; + case GDScriptTokenizer::Token::VAR: { + // Bind. + advance(); + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected bind name after "var".)")) { + return nullptr; } + pattern->pattern_type = PatternNode::PT_BIND; + pattern->bind = parse_identifier(); - if (p_class->extends_class.size()) { - for (int i = 0; i < p_class->extends_class.size(); i++) { - String sub = p_class->extends_class[i]; - if (script->get_subclasses().has(sub)) { - Ref<Script> subclass = script->get_subclasses()[sub]; //avoid reference from disappearing - script = subclass; - } else { - _set_error("Couldn't find the subclass: " + sub, p_class->line); - return; - } + PatternNode *root_pattern = p_root_pattern == nullptr ? pattern : p_root_pattern; + + if (p_root_pattern != nullptr) { + if (p_root_pattern->has_bind(pattern->bind->name)) { + push_error(vformat(R"(Bind variable name "%s" was already used in this pattern.)", pattern->bind->name)); + return nullptr; } } - } else { - if (p_class->extends_class.size() == 0) { - _set_error("Parser bug: undecidable inheritance.", p_class->line); - ERR_FAIL(); + if (current_suite->has_local(pattern->bind->name)) { + push_error(vformat(R"(There's already a %s named "%s" in this scope.)", current_suite->get_local(pattern->bind->name).get_name(), pattern->bind->name)); + return nullptr; } - //look around for the subclasses - - int extend_iter = 1; - String base = p_class->extends_class[0]; - ClassNode *p = p_class->owner; - Ref<GDScript> base_script; - - if (ScriptServer::is_global_class(base)) { - base_script = ResourceLoader::load(ScriptServer::get_global_class_path(base)); - if (!base_script.is_valid()) { - _set_error("The class \"" + base + "\" couldn't be fully loaded (script error or cyclic dependency).", p_class->line); - return; - } - p = nullptr; - } else { - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { + + root_pattern->binds[pattern->bind->name] = pattern->bind; + + } break; + case GDScriptTokenizer::Token::UNDERSCORE: + // Wildcard. + advance(); + pattern->pattern_type = PatternNode::PT_WILDCARD; + break; + case GDScriptTokenizer::Token::PERIOD_PERIOD: + // Rest. + advance(); + pattern->pattern_type = PatternNode::PT_REST; + break; + case GDScriptTokenizer::Token::BRACKET_OPEN: { + // Array. + advance(); + pattern->pattern_type = PatternNode::PT_ARRAY; + + if (!check(GDScriptTokenizer::Token::BRACKET_CLOSE)) { + do { + PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern); + if (sub_pattern == nullptr) { continue; } - String name = s.get_slice("/", 1); - if (name == base) { - String singleton_path = ProjectSettings::get_singleton()->get(s); - if (singleton_path.begins_with("*")) { - singleton_path = singleton_path.right(1); - } - if (!singleton_path.begins_with("res://")) { - singleton_path = "res://" + singleton_path; - } - base_script = ResourceLoader::load(singleton_path); - if (!base_script.is_valid()) { - _set_error("Class '" + base + "' could not be fully loaded (script error or cyclic inheritance).", p_class->line); - return; - } - p = nullptr; + if (pattern->rest_used) { + push_error(R"(The ".." pattern must be the last element in the pattern array.)"); + } else if (sub_pattern->pattern_type == PatternNode::PT_REST) { + pattern->rest_used = true; } - } + pattern->array.push_back(sub_pattern); + } while (match(GDScriptTokenizer::Token::COMMA)); } - - while (p) { - bool found = false; - - for (int i = 0; i < p->subclasses.size(); i++) { - if (p->subclasses[i]->name == base) { - ClassNode *test = p->subclasses[i]; - while (test) { - if (test == p_class) { - _set_error("Cyclic inheritance.", test->line); - return; + consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected "]" to close the array pattern.)"); + break; + } + case GDScriptTokenizer::Token::BRACE_OPEN: { + // Dictionary. + advance(); + pattern->pattern_type = PatternNode::PT_DICTIONARY; + + if (!check(GDScriptTokenizer::Token::BRACE_CLOSE) && !is_at_end()) { + do { + if (match(GDScriptTokenizer::Token::PERIOD_PERIOD)) { + // Rest. + if (pattern->rest_used) { + push_error(R"(The ".." pattern must be the last element in the pattern dictionary.)"); + } else { + PatternNode *sub_pattern = alloc_node<PatternNode>(); + sub_pattern->pattern_type = PatternNode::PT_REST; + pattern->dictionary.push_back({ nullptr, sub_pattern }); + pattern->rest_used = true; + } + } else { + ExpressionNode *key = parse_expression(false); + if (key == nullptr) { + push_error(R"(Expected expression as key for dictionary pattern.)"); + } + if (match(GDScriptTokenizer::Token::COLON)) { + // Value pattern. + PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern); + if (sub_pattern == nullptr) { + continue; } - if (test->base_type.kind == DataType::CLASS) { - test = test->base_type.class_type; + if (pattern->rest_used) { + push_error(R"(The ".." pattern must be the last element in the pattern dictionary.)"); + } else if (sub_pattern->pattern_type == PatternNode::PT_REST) { + push_error(R"(The ".." pattern cannot be used as a value.)"); } else { - break; + pattern->dictionary.push_back({ key, sub_pattern }); } - } - found = true; - if (extend_iter < p_class->extends_class.size()) { - // Keep looking at current classes if possible - base = p_class->extends_class[extend_iter++]; - p = p->subclasses[i]; } else { - base_class = p->subclasses[i]; - } - break; - } - } - - if (base_class) { - break; - } - if (found) { - continue; - } - - if (p->constant_expressions.has(base)) { - if (p->constant_expressions[base].expression->type != Node::TYPE_CONSTANT) { - _set_error("Couldn't resolve the constant \"" + base + "\".", p_class->line); - return; - } - const ConstantNode *cn = static_cast<const ConstantNode *>(p->constant_expressions[base].expression); - base_script = cn->value; - if (base_script.is_null()) { - _set_error("Constant isn't a class: " + base, p_class->line); - return; - } - break; - } - - p = p->owner; - } - - if (base_script.is_valid()) { - String ident = base; - Ref<GDScript> find_subclass = base_script; - - for (int i = extend_iter; i < p_class->extends_class.size(); i++) { - String subclass = p_class->extends_class[i]; - - ident += ("." + subclass); - - if (find_subclass->get_subclasses().has(subclass)) { - find_subclass = find_subclass->get_subclasses()[subclass]; - } else if (find_subclass->get_constants().has(subclass)) { - Ref<GDScript> new_base_class = find_subclass->get_constants()[subclass]; - if (new_base_class.is_null()) { - _set_error("Constant isn't a class: " + ident, p_class->line); - return; + // Key match only. + pattern->dictionary.push_back({ key, nullptr }); } - find_subclass = new_base_class; - } else { - _set_error("Couldn't find the subclass: " + ident, p_class->line); - return; } - } - - script = find_subclass; - - } else if (!base_class) { - if (p_class->extends_class.size() > 1) { - _set_error("Invalid inheritance (unknown class + subclasses).", p_class->line); - return; - } - //if not found, try engine classes - if (!GDScriptLanguage::get_singleton()->get_global_map().has(base)) { - _set_error("Unknown class: \"" + base + "\"", p_class->line); - return; - } - - native = base; + } while (match(GDScriptTokenizer::Token::COMMA)); } + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected "}" to close the dictionary pattern.)"); + break; } - - if (base_class) { - p_class->base_type.has_type = true; - p_class->base_type.kind = DataType::CLASS; - p_class->base_type.class_type = base_class; - } else if (script.is_valid()) { - p_class->base_type.has_type = true; - p_class->base_type.kind = DataType::GDSCRIPT; - p_class->base_type.script_type = script; - p_class->base_type.native_type = script->get_instance_base_type(); - } else if (native != StringName()) { - p_class->base_type.has_type = true; - p_class->base_type.kind = DataType::NATIVE; - p_class->base_type.native_type = native; - } else { - _set_error("Couldn't determine inheritance.", p_class->line); - return; - } - - } else { - // without extends, implicitly extend Reference - p_class->base_type.has_type = true; - p_class->base_type.kind = DataType::NATIVE; - p_class->base_type.native_type = "Reference"; - } - - if (p_recursive) { - // Recursively determine subclasses - for (int i = 0; i < p_class->subclasses.size(); i++) { - _determine_inheritance(p_class->subclasses[i], p_recursive); - } - } -} - -String GDScriptParser::DataType::to_string() const { - if (!has_type) { - return "var"; - } - switch (kind) { - case BUILTIN: { - if (builtin_type == Variant::NIL) { - return "null"; - } - return Variant::get_type_name(builtin_type); - } break; - case NATIVE: { - if (is_meta_type) { - return "GDScriptNativeClass"; - } - return native_type.operator String(); - } break; - - case GDSCRIPT: { - Ref<GDScript> gds = script_type; - const String &gds_class = gds->get_script_class_name(); - if (!gds_class.empty()) { - return gds_class; + default: { + // Expression. + ExpressionNode *expression = parse_expression(false); + if (expression == nullptr) { + push_error(R"(Expected expression for match pattern.)"); + } else { + pattern->pattern_type = PatternNode::PT_EXPRESSION; + pattern->expression = expression; } - [[fallthrough]]; + break; } - case SCRIPT: { - if (is_meta_type) { - return script_type->get_class_name().operator String(); - } - String name = script_type->get_name(); - if (name != String()) { - return name; - } - name = script_type->get_path().get_file(); - if (name != String()) { - return name; - } - return native_type.operator String(); - } break; - case CLASS: { - ERR_FAIL_COND_V(!class_type, String()); - if (is_meta_type) { - return "GDScript"; - } - if (class_type->name == StringName()) { - return "self"; - } - return class_type->name.operator String(); - } break; - case UNRESOLVED: { - } break; } - return "Unresolved"; + return pattern; } -bool GDScriptParser::_parse_type(DataType &r_type, bool p_can_be_void) { - tokenizer->advance(); - r_type.has_type = true; - - bool finished = false; - bool can_index = false; - String full_name; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = StringName(); - completion_type = COMPLETION_TYPE_HINT; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = 0; - completion_block = current_block; - completion_found = true; - completion_ident_is_call = p_can_be_void; - tokenizer->advance(); - } +bool GDScriptParser::PatternNode::has_bind(const StringName &p_name) { + return binds.has(p_name); +} - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_PR_VOID: { - if (!p_can_be_void) { - return false; - } - r_type.kind = DataType::BUILTIN; - r_type.builtin_type = Variant::NIL; - } break; - case GDScriptTokenizer::TK_BUILT_IN_TYPE: { - r_type.builtin_type = tokenizer->get_token_type(); - if (tokenizer->get_token_type() == Variant::OBJECT) { - r_type.kind = DataType::NATIVE; - r_type.native_type = "Object"; - } else { - r_type.kind = DataType::BUILTIN; - } - } break; - case GDScriptTokenizer::TK_IDENTIFIER: { - r_type.native_type = tokenizer->get_token_identifier(); - if (ClassDB::class_exists(r_type.native_type) || ClassDB::class_exists("_" + r_type.native_type.operator String())) { - r_type.kind = DataType::NATIVE; - } else { - r_type.kind = DataType::UNRESOLVED; - can_index = true; - full_name = r_type.native_type; - } - } break; - default: { - return false; - } - } +GDScriptParser::IdentifierNode *GDScriptParser::PatternNode::get_bind(const StringName &p_name) { + return binds[p_name]; +} - tokenizer->advance(); +GDScriptParser::WhileNode *GDScriptParser::parse_while() { + WhileNode *n_while = alloc_node<WhileNode>(); - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = r_type.native_type; - completion_type = COMPLETION_TYPE_HINT; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = 0; - completion_block = current_block; - completion_found = true; - completion_ident_is_call = p_can_be_void; - tokenizer->advance(); + n_while->condition = parse_expression(false); + if (n_while->condition == nullptr) { + push_error(R"(Expected conditional expression after "while".)"); } - if (can_index) { - while (!finished) { - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_PERIOD: { - if (!can_index) { - _set_error("Unexpected \".\"."); - return false; - } - can_index = false; - tokenizer->advance(); - } break; - case GDScriptTokenizer::TK_IDENTIFIER: { - if (can_index) { - _set_error("Unexpected identifier."); - return false; - } + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "while" condition.)"); - StringName id; - bool has_completion = _get_completable_identifier(COMPLETION_TYPE_HINT_INDEX, id); - if (id == StringName()) { - id = "@temp"; - } + // Save break/continue state. + bool could_break = can_break; + bool could_continue = can_continue; - full_name += "." + id.operator String(); - can_index = true; - if (has_completion) { - completion_cursor = full_name; - } - } break; - default: { - finished = true; - } break; - } - } + // Allow break/continue. + can_break = true; + can_continue = true; - if (tokenizer->get_token(-1) == GDScriptTokenizer::TK_PERIOD) { - _set_error("Expected a subclass identifier."); - return false; - } + n_while->loop = parse_suite(R"("while" block)"); - r_type.native_type = full_name; - } + // Reset break/continue state. + can_break = could_break; + can_continue = could_continue; - return true; + return n_while; } -GDScriptParser::DataType GDScriptParser::_resolve_type(const DataType &p_source, int p_line) { - if (!p_source.has_type) { - return p_source; - } - if (p_source.kind != DataType::UNRESOLVED) { - return p_source; +GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_precedence, bool p_can_assign, bool p_stop_on_assign) { + // Switch multiline mode on for grouping tokens. + // Do this early to avoid the tokenizer generating whitespace tokens. + switch (current.type) { + case GDScriptTokenizer::Token::PARENTHESIS_OPEN: + case GDScriptTokenizer::Token::BRACE_OPEN: + case GDScriptTokenizer::Token::BRACKET_OPEN: + push_multiline(true); + break; + default: + break; // Nothing to do. } - Vector<String> full_name = p_source.native_type.operator String().split(".", false); - int name_part = 0; + // Completion can appear whenever an expression is expected. + make_completion_context(COMPLETION_IDENTIFIER, nullptr); - DataType result; - result.has_type = true; + GDScriptTokenizer::Token token = advance(); + ParseFunction prefix_rule = get_rule(token.type)->prefix; - while (name_part < full_name.size()) { - bool found = false; - StringName id = full_name[name_part]; - DataType base_type = result; + if (prefix_rule == nullptr) { + // Expected expression. Let the caller give the proper error message. + return nullptr; + } - ClassNode *p = nullptr; - if (name_part == 0) { - if (ScriptServer::is_global_class(id)) { - String script_path = ScriptServer::get_global_class_path(id); - if (script_path == self_path) { - result.kind = DataType::CLASS; - result.class_type = static_cast<ClassNode *>(head); - } else { - Ref<Script> script = ResourceLoader::load(script_path); - Ref<GDScript> gds = script; - if (gds.is_valid()) { - if (!gds->is_valid()) { - _set_error("The class \"" + id + "\" couldn't be fully loaded (script error or cyclic dependency).", p_line); - return DataType(); - } - result.kind = DataType::GDSCRIPT; - result.script_type = gds; - } else if (script.is_valid()) { - result.kind = DataType::SCRIPT; - result.script_type = script; - } else { - _set_error("The class \"" + id + "\" was found in global scope, but its script couldn't be loaded.", p_line); - return DataType(); - } - } - name_part++; - continue; - } - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - String singleton_path; - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; - } - String name = s.get_slice("/", 1); - if (name == id) { - singleton_path = ProjectSettings::get_singleton()->get(s); - if (singleton_path.begins_with("*")) { - singleton_path = singleton_path.right(1); - } - if (!singleton_path.begins_with("res://")) { - singleton_path = "res://" + singleton_path; - } - break; - } - } - if (!singleton_path.empty()) { - Ref<Script> script = ResourceLoader::load(singleton_path); - Ref<GDScript> gds = script; - if (gds.is_valid()) { - if (!gds->is_valid()) { - _set_error("Class '" + id + "' could not be fully loaded (script error or cyclic inheritance).", p_line); - return DataType(); - } - result.kind = DataType::GDSCRIPT; - result.script_type = gds; - } else if (script.is_valid()) { - result.kind = DataType::SCRIPT; - result.script_type = script; - } else { - _set_error("Couldn't fully load singleton script '" + id + "' (possible cyclic reference or parse error).", p_line); - return DataType(); - } - name_part++; - continue; - } + ExpressionNode *previous_operand = (this->*prefix_rule)(nullptr, p_can_assign); - p = current_class; - } else if (base_type.kind == DataType::CLASS) { - p = base_type.class_type; + while (p_precedence <= get_rule(current.type)->precedence) { + if (p_stop_on_assign && current.type == GDScriptTokenizer::Token::EQUAL) { + return previous_operand; } - while (p) { - if (p->constant_expressions.has(id)) { - if (p->constant_expressions[id].expression->type != Node::TYPE_CONSTANT) { - _set_error("Parser bug: unresolved constant.", p_line); - ERR_FAIL_V(result); - } - const ConstantNode *cn = static_cast<const ConstantNode *>(p->constant_expressions[id].expression); - Ref<GDScript> gds = cn->value; - if (gds.is_valid()) { - result.kind = DataType::GDSCRIPT; - result.script_type = gds; - found = true; - } else { - Ref<Script> scr = cn->value; - if (scr.is_valid()) { - result.kind = DataType::SCRIPT; - result.script_type = scr; - found = true; - } - } + // Also switch multiline mode on here for infix operators. + switch (current.type) { + // case GDScriptTokenizer::Token::BRACE_OPEN: // Not an infix operator. + case GDScriptTokenizer::Token::PARENTHESIS_OPEN: + case GDScriptTokenizer::Token::BRACKET_OPEN: + push_multiline(true); break; - } - - // Inner classes - ClassNode *outer_class = p; - while (outer_class) { - if (outer_class->name == id) { - found = true; - result.kind = DataType::CLASS; - result.class_type = outer_class; - break; - } - for (int i = 0; i < outer_class->subclasses.size(); i++) { - if (outer_class->subclasses[i] == p) { - continue; - } - if (outer_class->subclasses[i]->name == id) { - found = true; - result.kind = DataType::CLASS; - result.class_type = outer_class->subclasses[i]; - break; - } - } - if (found) { - break; - } - outer_class = outer_class->owner; - } - - if (!found && p->base_type.kind == DataType::CLASS) { - p = p->base_type.class_type; - } else { - base_type = p->base_type; - break; - } - } - - // Still look for class constants in parent scripts - if (!found && (base_type.kind == DataType::GDSCRIPT || base_type.kind == DataType::SCRIPT)) { - Ref<Script> scr = base_type.script_type; - ERR_FAIL_COND_V(scr.is_null(), result); - while (scr.is_valid()) { - Map<StringName, Variant> constants; - scr->get_constants(&constants); - - if (constants.has(id)) { - Ref<GDScript> gds = constants[id]; - - if (gds.is_valid()) { - result.kind = DataType::GDSCRIPT; - result.script_type = gds; - found = true; - } else { - Ref<Script> scr2 = constants[id]; - if (scr2.is_valid()) { - result.kind = DataType::SCRIPT; - result.script_type = scr2; - found = true; - } - } - } - if (found) { - break; - } else { - scr = scr->get_base_script(); - } - } + default: + break; // Nothing to do. } + token = advance(); + ParseFunction infix_rule = get_rule(token.type)->infix; + previous_operand = (this->*infix_rule)(previous_operand, p_can_assign); + } - if (!found && !for_completion) { - String base; - if (name_part == 0) { - base = "self"; - } else { - base = result.to_string(); - } - _set_error("The identifier \"" + String(id) + "\" isn't a valid type (not a script or class), or couldn't be found on base \"" + - base + "\".", - p_line); - return DataType(); - } + return previous_operand; +} - name_part++; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_expression(bool p_can_assign, bool p_stop_on_assign) { + return parse_precedence(PREC_ASSIGNMENT, p_can_assign, p_stop_on_assign); +} - return result; +GDScriptParser::IdentifierNode *GDScriptParser::parse_identifier() { + return static_cast<IdentifierNode *>(parse_identifier(nullptr, false)); } -GDScriptParser::DataType GDScriptParser::_type_from_variant(const Variant &p_value) const { - DataType result; - result.has_type = true; - result.is_constant = true; - result.kind = DataType::BUILTIN; - result.builtin_type = p_value.get_type(); +GDScriptParser::ExpressionNode *GDScriptParser::parse_identifier(ExpressionNode *p_previous_operand, bool p_can_assign) { + if (!previous.is_identifier()) { + ERR_FAIL_V_MSG(nullptr, "Parser bug: parsing literal node without literal token."); + } + IdentifierNode *identifier = alloc_node<IdentifierNode>(); + identifier->name = previous.get_identifier(); - if (result.builtin_type == Variant::OBJECT) { - Object *obj = p_value.operator Object *(); - if (!obj) { - return DataType(); - } - result.native_type = obj->get_class_name(); - Ref<Script> scr = p_value; - if (scr.is_valid()) { - result.is_meta_type = true; - } else { - result.is_meta_type = false; - scr = obj->get_script(); - } - if (scr.is_valid()) { - result.script_type = scr; - Ref<GDScript> gds = scr; - if (gds.is_valid()) { - result.kind = DataType::GDSCRIPT; - } else { - result.kind = DataType::SCRIPT; - } - result.native_type = scr->get_instance_base_type(); - } else { - result.kind = DataType::NATIVE; + if (current_suite != nullptr && current_suite->has_local(identifier->name)) { + const SuiteNode::Local &declaration = current_suite->get_local(identifier->name); + switch (declaration.type) { + case SuiteNode::Local::CONSTANT: + identifier->source = IdentifierNode::LOCAL_CONSTANT; + identifier->constant_source = declaration.constant; + declaration.constant->usages++; + break; + case SuiteNode::Local::VARIABLE: + identifier->source = IdentifierNode::LOCAL_VARIABLE; + identifier->variable_source = declaration.variable; + declaration.variable->usages++; + break; + case SuiteNode::Local::PARAMETER: + identifier->source = IdentifierNode::FUNCTION_PARAMETER; + identifier->parameter_source = declaration.parameter; + declaration.parameter->usages++; + break; + case SuiteNode::Local::FOR_VARIABLE: + identifier->source = IdentifierNode::LOCAL_ITERATOR; + identifier->bind_source = declaration.bind; + declaration.bind->usages++; + break; + case SuiteNode::Local::PATTERN_BIND: + identifier->source = IdentifierNode::LOCAL_BIND; + identifier->bind_source = declaration.bind; + declaration.bind->usages++; + break; + case SuiteNode::Local::UNDEFINED: + ERR_FAIL_V_MSG(nullptr, "Undefined local found."); } } - return result; + return identifier; } -GDScriptParser::DataType GDScriptParser::_type_from_property(const PropertyInfo &p_property, bool p_nil_is_variant) const { - DataType ret; - if (p_property.type == Variant::NIL && (p_nil_is_variant || (p_property.usage & PROPERTY_USAGE_NIL_IS_VARIANT))) { - // Variant - return ret; - } - ret.has_type = true; - ret.builtin_type = p_property.type; - if (p_property.type == Variant::OBJECT) { - ret.kind = DataType::NATIVE; - ret.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name; - } else { - ret.kind = DataType::BUILTIN; - } - return ret; +GDScriptParser::LiteralNode *GDScriptParser::parse_literal() { + return static_cast<LiteralNode *>(parse_literal(nullptr, false)); } -GDScriptParser::DataType GDScriptParser::_type_from_gdtype(const GDScriptDataType &p_gdtype) const { - DataType result; - if (!p_gdtype.has_type) { - return result; +GDScriptParser::ExpressionNode *GDScriptParser::parse_literal(ExpressionNode *p_previous_operand, bool p_can_assign) { + if (previous.type != GDScriptTokenizer::Token::LITERAL) { + push_error("Parser bug: parsing literal node without literal token."); + ERR_FAIL_V_MSG(nullptr, "Parser bug: parsing literal node without literal token."); } - result.has_type = true; - result.builtin_type = p_gdtype.builtin_type; - result.native_type = p_gdtype.native_type; - result.script_type = p_gdtype.script_type; - - switch (p_gdtype.kind) { - case GDScriptDataType::UNINITIALIZED: { - ERR_PRINT("Uninitialized datatype. Please report a bug."); - } break; - case GDScriptDataType::BUILTIN: { - result.kind = DataType::BUILTIN; - } break; - case GDScriptDataType::NATIVE: { - result.kind = DataType::NATIVE; - } break; - case GDScriptDataType::GDSCRIPT: { - result.kind = DataType::GDSCRIPT; - } break; - case GDScriptDataType::SCRIPT: { - result.kind = DataType::SCRIPT; - } break; - } - return result; + LiteralNode *literal = alloc_node<LiteralNode>(); + literal->value = previous.literal; + return literal; } -GDScriptParser::DataType GDScriptParser::_get_operation_type(const Variant::Operator p_op, const DataType &p_a, const DataType &p_b, bool &r_valid) const { - if (!p_a.has_type || !p_b.has_type) { - r_valid = true; - return DataType(); +GDScriptParser::ExpressionNode *GDScriptParser::parse_self(ExpressionNode *p_previous_operand, bool p_can_assign) { + if (!current_function || current_function->is_static) { + push_error(R"(Cannot use "self" outside a non-static function.)"); } + SelfNode *self = alloc_node<SelfNode>(); + self->current_class = current_class; + return self; +} - Variant::Type a_type = p_a.kind == DataType::BUILTIN ? p_a.builtin_type : Variant::OBJECT; - Variant::Type b_type = p_b.kind == DataType::BUILTIN ? p_b.builtin_type : Variant::OBJECT; +GDScriptParser::ExpressionNode *GDScriptParser::parse_builtin_constant(ExpressionNode *p_previous_operand, bool p_can_assign) { + GDScriptTokenizer::Token::Type op_type = previous.type; + LiteralNode *constant = alloc_node<LiteralNode>(); - Variant a; - REF a_ref; - if (a_type == Variant::OBJECT) { - a_ref.instance(); - a = a_ref; - } else { - Callable::CallError err; - a = Variant::construct(a_type, nullptr, 0, err); - if (err.error != Callable::CallError::CALL_OK) { - r_valid = false; - return DataType(); - } - } - Variant b; - REF b_ref; - if (b_type == Variant::OBJECT) { - b_ref.instance(); - b = b_ref; - } else { - Callable::CallError err; - b = Variant::construct(b_type, nullptr, 0, err); - if (err.error != Callable::CallError::CALL_OK) { - r_valid = false; - return DataType(); - } + switch (op_type) { + case GDScriptTokenizer::Token::CONST_PI: + constant->value = Math_PI; + break; + case GDScriptTokenizer::Token::CONST_TAU: + constant->value = Math_TAU; + break; + case GDScriptTokenizer::Token::CONST_INF: + constant->value = Math_INF; + break; + case GDScriptTokenizer::Token::CONST_NAN: + constant->value = Math_NAN; + break; + default: + return nullptr; // Unreachable. } - // Avoid division by zero - if (a_type == Variant::INT || a_type == Variant::FLOAT) { - Variant::evaluate(Variant::OP_ADD, a, 1, a, r_valid); - } - if (b_type == Variant::INT || b_type == Variant::FLOAT) { - Variant::evaluate(Variant::OP_ADD, b, 1, b, r_valid); - } - if (a_type == Variant::STRING && b_type != Variant::ARRAY) { - a = "%s"; // Work around for formatting operator (%) - } + return constant; +} - Variant ret; - Variant::evaluate(p_op, a, b, ret, r_valid); +GDScriptParser::ExpressionNode *GDScriptParser::parse_unary_operator(ExpressionNode *p_previous_operand, bool p_can_assign) { + GDScriptTokenizer::Token::Type op_type = previous.type; + UnaryOpNode *operation = alloc_node<UnaryOpNode>(); - if (r_valid) { - return _type_from_variant(ret); + switch (op_type) { + case GDScriptTokenizer::Token::MINUS: + operation->operation = UnaryOpNode::OP_NEGATIVE; + operation->variant_op = Variant::OP_NEGATE; + operation->operand = parse_precedence(PREC_SIGN, false); + break; + case GDScriptTokenizer::Token::PLUS: + operation->operation = UnaryOpNode::OP_POSITIVE; + operation->variant_op = Variant::OP_POSITIVE; + operation->operand = parse_precedence(PREC_SIGN, false); + break; + case GDScriptTokenizer::Token::TILDE: + operation->operation = UnaryOpNode::OP_COMPLEMENT; + operation->variant_op = Variant::OP_BIT_NEGATE; + operation->operand = parse_precedence(PREC_BIT_NOT, false); + break; + case GDScriptTokenizer::Token::NOT: + case GDScriptTokenizer::Token::BANG: + operation->operation = UnaryOpNode::OP_LOGIC_NOT; + operation->variant_op = Variant::OP_NOT; + operation->operand = parse_precedence(PREC_LOGIC_NOT, false); + break; + default: + return nullptr; // Unreachable. } - return DataType(); + return operation; } -Variant::Operator GDScriptParser::_get_variant_operation(const OperatorNode::Operator &p_op) const { - switch (p_op) { - case OperatorNode::OP_NEG: { - return Variant::OP_NEGATE; - } break; - case OperatorNode::OP_POS: { - return Variant::OP_POSITIVE; - } break; - case OperatorNode::OP_NOT: { - return Variant::OP_NOT; - } break; - case OperatorNode::OP_BIT_INVERT: { - return Variant::OP_BIT_NEGATE; - } break; - case OperatorNode::OP_IN: { - return Variant::OP_IN; - } break; - case OperatorNode::OP_EQUAL: { - return Variant::OP_EQUAL; - } break; - case OperatorNode::OP_NOT_EQUAL: { - return Variant::OP_NOT_EQUAL; - } break; - case OperatorNode::OP_LESS: { - return Variant::OP_LESS; - } break; - case OperatorNode::OP_LESS_EQUAL: { - return Variant::OP_LESS_EQUAL; - } break; - case OperatorNode::OP_GREATER: { - return Variant::OP_GREATER; - } break; - case OperatorNode::OP_GREATER_EQUAL: { - return Variant::OP_GREATER_EQUAL; - } break; - case OperatorNode::OP_AND: { - return Variant::OP_AND; - } break; - case OperatorNode::OP_OR: { - return Variant::OP_OR; - } break; - case OperatorNode::OP_ASSIGN_ADD: - case OperatorNode::OP_ADD: { - return Variant::OP_ADD; - } break; - case OperatorNode::OP_ASSIGN_SUB: - case OperatorNode::OP_SUB: { - return Variant::OP_SUBTRACT; - } break; - case OperatorNode::OP_ASSIGN_MUL: - case OperatorNode::OP_MUL: { - return Variant::OP_MULTIPLY; - } break; - case OperatorNode::OP_ASSIGN_DIV: - case OperatorNode::OP_DIV: { - return Variant::OP_DIVIDE; - } break; - case OperatorNode::OP_ASSIGN_MOD: - case OperatorNode::OP_MOD: { - return Variant::OP_MODULE; - } break; - case OperatorNode::OP_ASSIGN_BIT_AND: - case OperatorNode::OP_BIT_AND: { - return Variant::OP_BIT_AND; - } break; - case OperatorNode::OP_ASSIGN_BIT_OR: - case OperatorNode::OP_BIT_OR: { - return Variant::OP_BIT_OR; - } break; - case OperatorNode::OP_ASSIGN_BIT_XOR: - case OperatorNode::OP_BIT_XOR: { - return Variant::OP_BIT_XOR; - } break; - case OperatorNode::OP_ASSIGN_SHIFT_LEFT: - case OperatorNode::OP_SHIFT_LEFT: { - return Variant::OP_SHIFT_LEFT; - } - case OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - case OperatorNode::OP_SHIFT_RIGHT: { - return Variant::OP_SHIFT_RIGHT; - } - default: { - return Variant::OP_MAX; - } break; - } -} +GDScriptParser::ExpressionNode *GDScriptParser::parse_binary_operator(ExpressionNode *p_previous_operand, bool p_can_assign) { + GDScriptTokenizer::Token op = previous; + BinaryOpNode *operation = alloc_node<BinaryOpNode>(); -bool GDScriptParser::_is_type_compatible(const DataType &p_container, const DataType &p_expression, bool p_allow_implicit_conversion) const { - // Ignore for completion - if (!check_types || for_completion) { - return true; + Precedence precedence = (Precedence)(get_rule(op.type)->precedence + 1); + operation->left_operand = p_previous_operand; + operation->right_operand = parse_precedence(precedence, false); + + if (operation->right_operand == nullptr) { + push_error(vformat(R"(Expected expression after "%s" operator.")", op.get_name())); } - // Can't test if not all have type - if (!p_container.has_type || !p_expression.has_type) { - return true; + + // TODO: Also for unary, ternary, and assignment. + switch (op.type) { + case GDScriptTokenizer::Token::PLUS: + operation->operation = BinaryOpNode::OP_ADDITION; + operation->variant_op = Variant::OP_ADD; + break; + case GDScriptTokenizer::Token::MINUS: + operation->operation = BinaryOpNode::OP_SUBTRACTION; + operation->variant_op = Variant::OP_SUBTRACT; + break; + case GDScriptTokenizer::Token::STAR: + operation->operation = BinaryOpNode::OP_MULTIPLICATION; + operation->variant_op = Variant::OP_MULTIPLY; + break; + case GDScriptTokenizer::Token::SLASH: + operation->operation = BinaryOpNode::OP_DIVISION; + operation->variant_op = Variant::OP_DIVIDE; + break; + case GDScriptTokenizer::Token::PERCENT: + operation->operation = BinaryOpNode::OP_MODULO; + operation->variant_op = Variant::OP_MODULE; + break; + case GDScriptTokenizer::Token::LESS_LESS: + operation->operation = BinaryOpNode::OP_BIT_LEFT_SHIFT; + operation->variant_op = Variant::OP_SHIFT_LEFT; + break; + case GDScriptTokenizer::Token::GREATER_GREATER: + operation->operation = BinaryOpNode::OP_BIT_RIGHT_SHIFT; + operation->variant_op = Variant::OP_SHIFT_RIGHT; + break; + case GDScriptTokenizer::Token::AMPERSAND: + operation->operation = BinaryOpNode::OP_BIT_AND; + operation->variant_op = Variant::OP_BIT_AND; + break; + case GDScriptTokenizer::Token::PIPE: + operation->operation = BinaryOpNode::OP_BIT_OR; + operation->variant_op = Variant::OP_BIT_OR; + break; + case GDScriptTokenizer::Token::CARET: + operation->operation = BinaryOpNode::OP_BIT_XOR; + operation->variant_op = Variant::OP_BIT_XOR; + break; + case GDScriptTokenizer::Token::AND: + case GDScriptTokenizer::Token::AMPERSAND_AMPERSAND: + operation->operation = BinaryOpNode::OP_LOGIC_AND; + operation->variant_op = Variant::OP_AND; + break; + case GDScriptTokenizer::Token::OR: + case GDScriptTokenizer::Token::PIPE_PIPE: + operation->operation = BinaryOpNode::OP_LOGIC_OR; + operation->variant_op = Variant::OP_OR; + break; + case GDScriptTokenizer::Token::IS: + operation->operation = BinaryOpNode::OP_TYPE_TEST; + break; + case GDScriptTokenizer::Token::IN: + operation->operation = BinaryOpNode::OP_CONTENT_TEST; + operation->variant_op = Variant::OP_IN; + break; + case GDScriptTokenizer::Token::EQUAL_EQUAL: + operation->operation = BinaryOpNode::OP_COMP_EQUAL; + operation->variant_op = Variant::OP_EQUAL; + break; + case GDScriptTokenizer::Token::BANG_EQUAL: + operation->operation = BinaryOpNode::OP_COMP_NOT_EQUAL; + operation->variant_op = Variant::OP_NOT_EQUAL; + break; + case GDScriptTokenizer::Token::LESS: + operation->operation = BinaryOpNode::OP_COMP_LESS; + operation->variant_op = Variant::OP_LESS; + break; + case GDScriptTokenizer::Token::LESS_EQUAL: + operation->operation = BinaryOpNode::OP_COMP_LESS_EQUAL; + operation->variant_op = Variant::OP_LESS_EQUAL; + break; + case GDScriptTokenizer::Token::GREATER: + operation->operation = BinaryOpNode::OP_COMP_GREATER; + operation->variant_op = Variant::OP_GREATER; + break; + case GDScriptTokenizer::Token::GREATER_EQUAL: + operation->operation = BinaryOpNode::OP_COMP_GREATER_EQUAL; + operation->variant_op = Variant::OP_GREATER_EQUAL; + break; + default: + return nullptr; // Unreachable. } - // Should never get here unresolved - ERR_FAIL_COND_V(p_container.kind == DataType::UNRESOLVED, false); - ERR_FAIL_COND_V(p_expression.kind == DataType::UNRESOLVED, false); + return operation; +} - if (p_container.kind == DataType::BUILTIN && p_expression.kind == DataType::BUILTIN) { - bool valid = p_container.builtin_type == p_expression.builtin_type; - if (p_allow_implicit_conversion) { - valid = valid || Variant::can_convert_strict(p_expression.builtin_type, p_container.builtin_type); - } - return valid; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_ternary_operator(ExpressionNode *p_previous_operand, bool p_can_assign) { + // Only one ternary operation exists, so no abstraction here. + TernaryOpNode *operation = alloc_node<TernaryOpNode>(); + operation->true_expr = p_previous_operand; - if (p_container.kind == DataType::BUILTIN && p_container.builtin_type == Variant::OBJECT) { - // Object built-in is a special case, it's compatible with any object and with null - if (p_expression.kind == DataType::BUILTIN) { - return p_expression.builtin_type == Variant::NIL; - } - // If it's not a built-in, must be an object - return true; - } + operation->condition = parse_precedence(PREC_TERNARY, false); - if (p_container.kind == DataType::BUILTIN || (p_expression.kind == DataType::BUILTIN && p_expression.builtin_type != Variant::NIL)) { - // Can't mix built-ins with objects - return false; + if (operation->condition == nullptr) { + push_error(R"(Expected expression as ternary condition after "if".)"); } - // From now on everything is objects, check polymorphism - // The container must be the same class or a superclass of the expression + consume(GDScriptTokenizer::Token::ELSE, R"(Expected "else" after ternary operator condition.)"); - if (p_expression.kind == DataType::BUILTIN && p_expression.builtin_type == Variant::NIL) { - // Null can be assigned to object types - return true; + operation->false_expr = parse_precedence(PREC_TERNARY, false); + + return operation; +} + +GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode *p_previous_operand, bool p_can_assign) { + if (!p_can_assign) { + push_error("Assignment is not allowed inside an expression."); + return parse_expression(false); // Return the following expression. } - StringName expr_native; - Ref<Script> expr_script; - ClassNode *expr_class = nullptr; +#ifdef DEBUG_ENABLED + VariableNode *source_variable = nullptr; +#endif - switch (p_expression.kind) { - case DataType::NATIVE: { - if (p_container.kind != DataType::NATIVE) { - // Non-native type can't be a superclass of a native type - return false; - } - if (p_expression.is_meta_type) { - expr_native = GDScriptNativeClass::get_class_static(); - } else { - expr_native = p_expression.native_type; - } - } break; - case DataType::SCRIPT: - case DataType::GDSCRIPT: { - if (p_container.kind == DataType::CLASS) { - // This cannot be resolved without cyclic dependencies, so just bail out - return false; - } - if (p_expression.is_meta_type) { - expr_native = p_expression.script_type->get_class_name(); - } else { - expr_script = p_expression.script_type; - expr_native = expr_script->get_instance_base_type(); - } - } break; - case DataType::CLASS: { - if (p_expression.is_meta_type) { - expr_native = GDScript::get_class_static(); - } else { - expr_class = p_expression.class_type; - ClassNode *base = expr_class; - while (base->base_type.kind == DataType::CLASS) { - base = base->base_type.class_type; - } - expr_native = base->base_type.native_type; - expr_script = base->base_type.script_type; + switch (p_previous_operand->type) { + case Node::IDENTIFIER: { +#ifdef DEBUG_ENABLED + // Get source to store assignment count. + // Also remove one usage since assignment isn't usage. + IdentifierNode *id = static_cast<IdentifierNode *>(p_previous_operand); + switch (id->source) { + case IdentifierNode::LOCAL_VARIABLE: + + source_variable = id->variable_source; + id->variable_source->usages--; + break; + case IdentifierNode::LOCAL_CONSTANT: + id->constant_source->usages--; + break; + case IdentifierNode::FUNCTION_PARAMETER: + id->parameter_source->usages--; + break; + case IdentifierNode::LOCAL_ITERATOR: + case IdentifierNode::LOCAL_BIND: + id->bind_source->usages--; + break; + default: + break; } +#endif } break; - case DataType::BUILTIN: // Already handled above - case DataType::UNRESOLVED: // Not allowed, see above + case Node::SUBSCRIPT: + // Okay. break; + default: + push_error(R"(Only identifier, attribute access, and subscription access can be used as assignment target.)"); + return parse_expression(false); // Return the following expression. } - // Some classes are prefixed with `_` internally - if (!ClassDB::class_exists(expr_native)) { - expr_native = "_" + expr_native; + AssignmentNode *assignment = alloc_node<AssignmentNode>(); + make_completion_context(COMPLETION_ASSIGN, assignment); +#ifdef DEBUG_ENABLED + bool has_operator = true; +#endif + switch (previous.type) { + case GDScriptTokenizer::Token::EQUAL: + assignment->operation = AssignmentNode::OP_NONE; + assignment->variant_op = Variant::OP_MAX; +#ifdef DEBUG_ENABLED + has_operator = false; +#endif + break; + case GDScriptTokenizer::Token::PLUS_EQUAL: + assignment->operation = AssignmentNode::OP_ADDITION; + assignment->variant_op = Variant::OP_ADD; + break; + case GDScriptTokenizer::Token::MINUS_EQUAL: + assignment->operation = AssignmentNode::OP_SUBTRACTION; + assignment->variant_op = Variant::OP_SUBTRACT; + break; + case GDScriptTokenizer::Token::STAR_EQUAL: + assignment->operation = AssignmentNode::OP_MULTIPLICATION; + assignment->variant_op = Variant::OP_MULTIPLY; + break; + case GDScriptTokenizer::Token::SLASH_EQUAL: + assignment->operation = AssignmentNode::OP_DIVISION; + assignment->variant_op = Variant::OP_DIVIDE; + break; + case GDScriptTokenizer::Token::PERCENT_EQUAL: + assignment->operation = AssignmentNode::OP_MODULO; + assignment->variant_op = Variant::OP_MODULE; + break; + case GDScriptTokenizer::Token::LESS_LESS_EQUAL: + assignment->operation = AssignmentNode::OP_BIT_SHIFT_LEFT; + assignment->variant_op = Variant::OP_SHIFT_LEFT; + break; + case GDScriptTokenizer::Token::GREATER_GREATER_EQUAL: + assignment->operation = AssignmentNode::OP_BIT_SHIFT_RIGHT; + assignment->variant_op = Variant::OP_SHIFT_RIGHT; + break; + case GDScriptTokenizer::Token::AMPERSAND_EQUAL: + assignment->operation = AssignmentNode::OP_BIT_AND; + assignment->variant_op = Variant::OP_BIT_AND; + break; + case GDScriptTokenizer::Token::PIPE_EQUAL: + assignment->operation = AssignmentNode::OP_BIT_OR; + assignment->variant_op = Variant::OP_BIT_OR; + break; + case GDScriptTokenizer::Token::CARET_EQUAL: + assignment->operation = AssignmentNode::OP_BIT_XOR; + assignment->variant_op = Variant::OP_BIT_XOR; + break; + default: + break; // Unreachable. } + assignment->assignee = p_previous_operand; + assignment->assigned_value = parse_expression(false); - switch (p_container.kind) { - case DataType::NATIVE: { - if (p_container.is_meta_type) { - return ClassDB::is_parent_class(expr_native, GDScriptNativeClass::get_class_static()); - } else { - StringName container_native = ClassDB::class_exists(p_container.native_type) ? p_container.native_type : StringName("_" + p_container.native_type); - return ClassDB::is_parent_class(expr_native, container_native); - } - } break; - case DataType::SCRIPT: - case DataType::GDSCRIPT: { - if (p_container.is_meta_type) { - return ClassDB::is_parent_class(expr_native, GDScript::get_class_static()); - } - if (expr_class == head && p_container.script_type->get_path() == self_path) { - // Special case: container is self script and expression is self - return true; - } - while (expr_script.is_valid()) { - if (expr_script == p_container.script_type) { - return true; - } - expr_script = expr_script->get_base_script(); - } - return false; - } break; - case DataType::CLASS: { - if (p_container.is_meta_type) { - return ClassDB::is_parent_class(expr_native, GDScript::get_class_static()); - } - if (p_container.class_type == head && expr_script.is_valid() && expr_script->get_path() == self_path) { - // Special case: container is self and expression is self script - return true; - } - while (expr_class) { - if (expr_class == p_container.class_type) { - return true; - } - expr_class = expr_class->base_type.class_type; - } - return false; - } break; - case DataType::BUILTIN: // Already handled above - case DataType::UNRESOLVED: // Not allowed, see above - break; +#ifdef DEBUG_ENABLED + if (has_operator && source_variable != nullptr && source_variable->assignments == 0) { + push_warning(assignment, GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, source_variable->identifier->name); } +#endif - return false; + return assignment; } -GDScriptParser::Node *GDScriptParser::_get_default_value_for_type(const DataType &p_type, int p_line) { - Node *result; +GDScriptParser::ExpressionNode *GDScriptParser::parse_await(ExpressionNode *p_previous_operand, bool p_can_assign) { + AwaitNode *await = alloc_node<AwaitNode>(); + await->to_await = parse_precedence(PREC_AWAIT, false); - if (p_type.has_type && p_type.kind == DataType::BUILTIN && p_type.builtin_type != Variant::NIL && p_type.builtin_type != Variant::OBJECT) { - if (p_type.builtin_type == Variant::ARRAY) { - result = alloc_node<ArrayNode>(); - } else if (p_type.builtin_type == Variant::DICTIONARY) { - result = alloc_node<DictionaryNode>(); - } else { - ConstantNode *c = alloc_node<ConstantNode>(); - Callable::CallError err; - c->value = Variant::construct(p_type.builtin_type, nullptr, 0, err); - c->datatype = _type_from_variant(c->value); - result = c; - } - } else { - ConstantNode *c = alloc_node<ConstantNode>(); - c->value = Variant(); - c->datatype = _type_from_variant(c->value); - result = c; - } - - result->line = p_line; + current_function->is_coroutine = true; - return result; + return await; } -GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) { -#ifdef DEBUG_ENABLED - if (p_node->get_datatype().has_type && p_node->type != Node::TYPE_ARRAY && p_node->type != Node::TYPE_DICTIONARY) { -#else - if (p_node->get_datatype().has_type) { -#endif - return p_node->get_datatype(); - } - - DataType node_type; +GDScriptParser::ExpressionNode *GDScriptParser::parse_array(ExpressionNode *p_previous_operand, bool p_can_assign) { + ArrayNode *array = alloc_node<ArrayNode>(); - switch (p_node->type) { - case Node::TYPE_CONSTANT: { - node_type = _type_from_variant(static_cast<ConstantNode *>(p_node)->value); - } break; - case Node::TYPE_TYPE: { - TypeNode *tn = static_cast<TypeNode *>(p_node); - node_type.has_type = true; - node_type.is_meta_type = true; - node_type.kind = DataType::BUILTIN; - node_type.builtin_type = tn->vtype; - } break; - case Node::TYPE_ARRAY: { - node_type.has_type = true; - node_type.kind = DataType::BUILTIN; - node_type.builtin_type = Variant::ARRAY; -#ifdef DEBUG_ENABLED - // Check stuff inside the array - ArrayNode *an = static_cast<ArrayNode *>(p_node); - for (int i = 0; i < an->elements.size(); i++) { - _reduce_node_type(an->elements[i]); - } -#endif // DEBUG_ENABLED - } break; - case Node::TYPE_DICTIONARY: { - node_type.has_type = true; - node_type.kind = DataType::BUILTIN; - node_type.builtin_type = Variant::DICTIONARY; -#ifdef DEBUG_ENABLED - // Check stuff inside the dictionarty - DictionaryNode *dn = static_cast<DictionaryNode *>(p_node); - for (int i = 0; i < dn->elements.size(); i++) { - _reduce_node_type(dn->elements[i].key); - _reduce_node_type(dn->elements[i].value); - } -#endif // DEBUG_ENABLED - } break; - case Node::TYPE_SELF: { - node_type.has_type = true; - node_type.kind = DataType::CLASS; - node_type.class_type = current_class; - node_type.is_constant = true; - } break; - case Node::TYPE_IDENTIFIER: { - IdentifierNode *id = static_cast<IdentifierNode *>(p_node); - if (id->declared_block) { - node_type = id->declared_block->variables[id->name]->get_datatype(); - id->declared_block->variables[id->name]->usages += 1; - } else if (id->name == "#match_value") { - // It's a special id just for the match statetement, ignore + if (!check(GDScriptTokenizer::Token::BRACKET_CLOSE)) { + do { + if (check(GDScriptTokenizer::Token::BRACKET_CLOSE)) { + // Allow for trailing comma. break; - } else if (current_function && current_function->arguments.find(id->name) >= 0) { - int idx = current_function->arguments.find(id->name); - node_type = current_function->argument_types[idx]; - } else { - node_type = _reduce_identifier_type(nullptr, id->name, id->line, false); } - } break; - case Node::TYPE_CAST: { - CastNode *cn = static_cast<CastNode *>(p_node); - - DataType source_type = _reduce_node_type(cn->source_node); - cn->cast_type = _resolve_type(cn->cast_type, cn->line); - if (source_type.has_type) { - bool valid = false; - if (check_types) { - if (cn->cast_type.kind == DataType::BUILTIN && source_type.kind == DataType::BUILTIN) { - valid = Variant::can_convert(source_type.builtin_type, cn->cast_type.builtin_type); - } - if (cn->cast_type.kind != DataType::BUILTIN && source_type.kind != DataType::BUILTIN) { - valid = _is_type_compatible(cn->cast_type, source_type) || _is_type_compatible(source_type, cn->cast_type); - } - if (!valid) { - _set_error("Invalid cast. Cannot convert from \"" + source_type.to_string() + - "\" to \"" + cn->cast_type.to_string() + "\".", - cn->line); - return DataType(); - } - } + ExpressionNode *element = parse_expression(false); + if (element == nullptr) { + push_error(R"(Expected expression as array element.)"); } else { -#ifdef DEBUG_ENABLED - _add_warning(GDScriptWarning::UNSAFE_CAST, cn->line, cn->cast_type.to_string()); -#endif // DEBUG_ENABLED - _mark_line_as_unsafe(cn->line); + array->elements.push_back(element); } + } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); + } + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected closing "]" after array elements.)"); - node_type = cn->cast_type; - - } break; - case Node::TYPE_OPERATOR: { - OperatorNode *op = static_cast<OperatorNode *>(p_node); - - switch (op->op) { - case OperatorNode::OP_CALL: - case OperatorNode::OP_PARENT_CALL: { - node_type = _reduce_function_call_type(op); - } break; - case OperatorNode::OP_YIELD: { - if (op->arguments.size() == 2) { - DataType base_type = _reduce_node_type(op->arguments[0]); - DataType signal_type = _reduce_node_type(op->arguments[1]); - // TODO: Check if signal exists when it's a constant - if (base_type.has_type && base_type.kind == DataType::BUILTIN && base_type.builtin_type != Variant::NIL && base_type.builtin_type != Variant::OBJECT) { - _set_error("The first argument of \"yield()\" must be an object.", op->line); - return DataType(); - } - if (signal_type.has_type && (signal_type.kind != DataType::BUILTIN || signal_type.builtin_type != Variant::STRING)) { - _set_error("The second argument of \"yield()\" must be a string.", op->line); - return DataType(); - } - } - // yield can return anything - node_type.has_type = false; - } break; - case OperatorNode::OP_IS: - case OperatorNode::OP_IS_BUILTIN: { - if (op->arguments.size() != 2) { - _set_error("Parser bug: binary operation without 2 arguments.", op->line); - ERR_FAIL_V(DataType()); - } - - DataType value_type = _reduce_node_type(op->arguments[0]); - DataType type_type = _reduce_node_type(op->arguments[1]); - - if (check_types && type_type.has_type) { - if (!type_type.is_meta_type && (type_type.kind != DataType::NATIVE || !ClassDB::is_parent_class(type_type.native_type, "Script"))) { - _set_error("Invalid \"is\" test: the right operand isn't a type (neither a native type nor a script).", op->line); - return DataType(); - } - type_type.is_meta_type = false; // Test the actual type - if (!_is_type_compatible(type_type, value_type) && !_is_type_compatible(value_type, type_type)) { - if (op->op == OperatorNode::OP_IS) { - _set_error("A value of type \"" + value_type.to_string() + "\" will never be an instance of \"" + type_type.to_string() + "\".", op->line); - } else { - _set_error("A value of type \"" + value_type.to_string() + "\" will never be of type \"" + type_type.to_string() + "\".", op->line); - } - return DataType(); - } - } + return array; +} - node_type.has_type = true; - node_type.is_constant = true; - node_type.is_meta_type = false; - node_type.kind = DataType::BUILTIN; - node_type.builtin_type = Variant::BOOL; - } break; - // Unary operators - case OperatorNode::OP_NEG: - case OperatorNode::OP_POS: - case OperatorNode::OP_NOT: - case OperatorNode::OP_BIT_INVERT: { - DataType argument_type = _reduce_node_type(op->arguments[0]); - if (!argument_type.has_type) { - break; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_dictionary(ExpressionNode *p_previous_operand, bool p_can_assign) { + DictionaryNode *dictionary = alloc_node<DictionaryNode>(); - Variant::Operator var_op = _get_variant_operation(op->op); - bool valid = false; - node_type = _get_operation_type(var_op, argument_type, argument_type, valid); + bool decided_style = false; + if (!check(GDScriptTokenizer::Token::BRACE_CLOSE)) { + do { + if (check(GDScriptTokenizer::Token::BRACE_CLOSE)) { + // Allow for trailing comma. + break; + } - if (check_types && !valid) { - _set_error("Invalid operand type (\"" + argument_type.to_string() + - "\") to unary operator \"" + Variant::get_operator_name(var_op) + "\".", - op->line, op->column); - return DataType(); - } + // Key. + ExpressionNode *key = parse_expression(false, true); // Stop on "=" so we can check for Lua table style. - } break; - // Binary operators - case OperatorNode::OP_IN: - case OperatorNode::OP_EQUAL: - case OperatorNode::OP_NOT_EQUAL: - case OperatorNode::OP_LESS: - case OperatorNode::OP_LESS_EQUAL: - case OperatorNode::OP_GREATER: - case OperatorNode::OP_GREATER_EQUAL: - case OperatorNode::OP_AND: - case OperatorNode::OP_OR: - case OperatorNode::OP_ADD: - case OperatorNode::OP_SUB: - case OperatorNode::OP_MUL: - case OperatorNode::OP_DIV: - case OperatorNode::OP_MOD: - case OperatorNode::OP_SHIFT_LEFT: - case OperatorNode::OP_SHIFT_RIGHT: - case OperatorNode::OP_BIT_AND: - case OperatorNode::OP_BIT_OR: - case OperatorNode::OP_BIT_XOR: { - if (op->arguments.size() != 2) { - _set_error("Parser bug: binary operation without 2 arguments.", op->line); - ERR_FAIL_V(DataType()); - } + if (key == nullptr) { + push_error(R"(Expected expression as dictionary key.)"); + } - DataType argument_a_type = _reduce_node_type(op->arguments[0]); - DataType argument_b_type = _reduce_node_type(op->arguments[1]); - if (!argument_a_type.has_type || !argument_b_type.has_type) { - _mark_line_as_unsafe(op->line); + if (!decided_style) { + switch (current.type) { + case GDScriptTokenizer::Token::COLON: + dictionary->style = DictionaryNode::PYTHON_DICT; break; - } - - Variant::Operator var_op = _get_variant_operation(op->op); - bool valid = false; - node_type = _get_operation_type(var_op, argument_a_type, argument_b_type, valid); - - if (check_types && !valid) { - _set_error("Invalid operand types (\"" + argument_a_type.to_string() + "\" and \"" + - argument_b_type.to_string() + "\") to operator \"" + Variant::get_operator_name(var_op) + "\".", - op->line, op->column); - return DataType(); - } -#ifdef DEBUG_ENABLED - if (var_op == Variant::OP_DIVIDE && argument_a_type.kind == DataType::BUILTIN && argument_a_type.builtin_type == Variant::INT && - argument_b_type.kind == DataType::BUILTIN && argument_b_type.builtin_type == Variant::INT) { - _add_warning(GDScriptWarning::INTEGER_DIVISION, op->line); - } -#endif // DEBUG_ENABLED - - } break; - // Ternary operators - case OperatorNode::OP_TERNARY_IF: { - if (op->arguments.size() != 3) { - _set_error("Parser bug: ternary operation without 3 arguments."); - ERR_FAIL_V(DataType()); - } + case GDScriptTokenizer::Token::EQUAL: + dictionary->style = DictionaryNode::LUA_TABLE; + break; + default: + push_error(R"(Expected ":" or "=" after dictionary key.)"); + break; + } + decided_style = true; + } - DataType true_type = _reduce_node_type(op->arguments[1]); - DataType false_type = _reduce_node_type(op->arguments[2]); - // Check arguments[0] errors. - _reduce_node_type(op->arguments[0]); - - // If types are equal, then the expression is of the same type - // If they are compatible, return the broader type - if (true_type == false_type || _is_type_compatible(true_type, false_type)) { - node_type = true_type; - } else if (_is_type_compatible(false_type, true_type)) { - node_type = false_type; - } else { -#ifdef DEBUG_ENABLED - _add_warning(GDScriptWarning::INCOMPATIBLE_TERNARY, op->line); -#endif // DEBUG_ENABLED + switch (dictionary->style) { + case DictionaryNode::LUA_TABLE: + if (key != nullptr && key->type != Node::IDENTIFIER) { + push_error("Expected identifier as dictionary key."); } - } break; - // Assignment should never happen within an expression - case OperatorNode::OP_ASSIGN: - case OperatorNode::OP_ASSIGN_ADD: - case OperatorNode::OP_ASSIGN_SUB: - case OperatorNode::OP_ASSIGN_MUL: - case OperatorNode::OP_ASSIGN_DIV: - case OperatorNode::OP_ASSIGN_MOD: - case OperatorNode::OP_ASSIGN_SHIFT_LEFT: - case OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - case OperatorNode::OP_ASSIGN_BIT_AND: - case OperatorNode::OP_ASSIGN_BIT_OR: - case OperatorNode::OP_ASSIGN_BIT_XOR: - case OperatorNode::OP_INIT_ASSIGN: { - _set_error("Assignment inside an expression isn't allowed (parser bug?).", op->line); - return DataType(); - - } break; - case OperatorNode::OP_INDEX_NAMED: { - if (op->arguments.size() != 2) { - _set_error("Parser bug: named index with invalid arguments.", op->line); - ERR_FAIL_V(DataType()); - } - if (op->arguments[1]->type != Node::TYPE_IDENTIFIER) { - _set_error("Parser bug: named index without identifier argument.", op->line); - ERR_FAIL_V(DataType()); - } - - DataType base_type = _reduce_node_type(op->arguments[0]); - IdentifierNode *member_id = static_cast<IdentifierNode *>(op->arguments[1]); - - if (base_type.has_type) { - if (check_types && base_type.kind == DataType::BUILTIN) { - // Variant type, just test if it's possible - DataType result; - switch (base_type.builtin_type) { - case Variant::NIL: - case Variant::DICTIONARY: { - result.has_type = false; - } break; - default: { - Callable::CallError err; - Variant temp = Variant::construct(base_type.builtin_type, nullptr, 0, err); - - bool valid = false; - Variant res = temp.get(member_id->name.operator String(), &valid); - - if (valid) { - result = _type_from_variant(res); - } else if (check_types) { - _set_error("Can't get index \"" + String(member_id->name.operator String()) + "\" on base \"" + - base_type.to_string() + "\".", - op->line); - return DataType(); - } - } break; - } - result.is_constant = false; - node_type = result; + if (!match(GDScriptTokenizer::Token::EQUAL)) { + if (match(GDScriptTokenizer::Token::COLON)) { + push_error(R"(Expected "=" after dictionary key. Mixing dictionary styles is not allowed.)"); + advance(); // Consume wrong separator anyway. } else { - node_type = _reduce_identifier_type(&base_type, member_id->name, op->line, true); -#ifdef DEBUG_ENABLED - if (!node_type.has_type) { - _mark_line_as_unsafe(op->line); - _add_warning(GDScriptWarning::UNSAFE_PROPERTY_ACCESS, op->line, member_id->name.operator String(), base_type.to_string()); - } -#endif // DEBUG_ENABLED + push_error(R"(Expected "=" after dictionary key.)"); } - } else { - _mark_line_as_unsafe(op->line); } - if (error_set) { - return DataType(); - } - } break; - case OperatorNode::OP_INDEX: { - if (op->arguments[1]->type == Node::TYPE_CONSTANT) { - ConstantNode *cn = static_cast<ConstantNode *>(op->arguments[1]); - if (cn->value.get_type() == Variant::STRING) { - // Treat this as named indexing - - IdentifierNode *id = alloc_node<IdentifierNode>(); - id->name = cn->value.operator StringName(); - id->datatype = cn->datatype; - - op->op = OperatorNode::OP_INDEX_NAMED; - op->arguments.write[1] = id; - - return _reduce_node_type(op); + break; + case DictionaryNode::PYTHON_DICT: + if (!match(GDScriptTokenizer::Token::COLON)) { + if (match(GDScriptTokenizer::Token::EQUAL)) { + push_error(R"(Expected ":" after dictionary key. Mixing dictionary styles is not allowed.)"); + advance(); // Consume wrong separator anyway. + } else { + push_error(R"(Expected ":" after dictionary key.)"); } } + break; + } - DataType base_type = _reduce_node_type(op->arguments[0]); - DataType index_type = _reduce_node_type(op->arguments[1]); - - if (!base_type.has_type) { - _mark_line_as_unsafe(op->line); - break; - } - - if (check_types && index_type.has_type) { - if (base_type.kind == DataType::BUILTIN) { - // Check if indexing is valid - bool error = index_type.kind != DataType::BUILTIN && base_type.builtin_type != Variant::DICTIONARY; - if (!error) { - switch (base_type.builtin_type) { - // Expect int or real as index - case Variant::PACKED_BYTE_ARRAY: - case Variant::PACKED_COLOR_ARRAY: - case Variant::PACKED_INT32_ARRAY: - case Variant::PACKED_INT64_ARRAY: - case Variant::PACKED_FLOAT32_ARRAY: - case Variant::PACKED_FLOAT64_ARRAY: - case Variant::PACKED_STRING_ARRAY: - case Variant::PACKED_VECTOR2_ARRAY: - case Variant::PACKED_VECTOR3_ARRAY: - case Variant::ARRAY: - case Variant::STRING: { - error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT; - } break; - // Expect String only - case Variant::RECT2: - case Variant::PLANE: - case Variant::QUAT: - case Variant::AABB: - case Variant::OBJECT: { - error = index_type.builtin_type != Variant::STRING; - } break; - // Expect String or number - case Variant::VECTOR2: - case Variant::VECTOR3: - case Variant::TRANSFORM2D: - case Variant::BASIS: - case Variant::TRANSFORM: { - error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::FLOAT && - index_type.builtin_type != Variant::STRING; - } break; - // Expect String or int - case Variant::COLOR: { - error = index_type.builtin_type != Variant::INT && index_type.builtin_type != Variant::STRING; - } break; - default: { - } - } - } - if (error) { - _set_error("Invalid index type (" + index_type.to_string() + ") for base \"" + base_type.to_string() + "\".", - op->line); - return DataType(); - } + // Value. + ExpressionNode *value = parse_expression(false); + if (value == nullptr) { + push_error(R"(Expected expression as dictionary value.)"); + } - if (op->arguments[1]->type == GDScriptParser::Node::TYPE_CONSTANT) { - ConstantNode *cn = static_cast<ConstantNode *>(op->arguments[1]); - // Index is a constant, just try it if possible - switch (base_type.builtin_type) { - // Arrays/string have variable indexing, can't test directly - case Variant::STRING: - case Variant::ARRAY: - case Variant::DICTIONARY: - case Variant::PACKED_BYTE_ARRAY: - case Variant::PACKED_COLOR_ARRAY: - case Variant::PACKED_INT32_ARRAY: - case Variant::PACKED_INT64_ARRAY: - case Variant::PACKED_FLOAT32_ARRAY: - case Variant::PACKED_FLOAT64_ARRAY: - case Variant::PACKED_STRING_ARRAY: - case Variant::PACKED_VECTOR2_ARRAY: - case Variant::PACKED_VECTOR3_ARRAY: { - break; - } - default: { - Callable::CallError err; - Variant temp = Variant::construct(base_type.builtin_type, nullptr, 0, err); - - bool valid = false; - Variant res = temp.get(cn->value, &valid); - - if (valid) { - node_type = _type_from_variant(res); - node_type.is_constant = false; - } else if (check_types) { - _set_error("Can't get index \"" + String(cn->value) + "\" on base \"" + - base_type.to_string() + "\".", - op->line); - return DataType(); - } - } break; - } - } else { - _mark_line_as_unsafe(op->line); - } - } else if (!for_completion && (index_type.kind != DataType::BUILTIN || index_type.builtin_type != Variant::STRING)) { - _set_error("Only strings can be used as an index in the base type \"" + base_type.to_string() + "\".", op->line); - return DataType(); - } - } - if (check_types && !node_type.has_type && base_type.kind == DataType::BUILTIN) { - // Can infer indexing type for some variant types - DataType result; - result.has_type = true; - result.kind = DataType::BUILTIN; - switch (base_type.builtin_type) { - // Can't index at all - case Variant::NIL: - case Variant::BOOL: - case Variant::INT: - case Variant::FLOAT: - case Variant::NODE_PATH: - case Variant::_RID: { - _set_error("Can't index on a value of type \"" + base_type.to_string() + "\".", op->line); - return DataType(); - } break; - // Return int - case Variant::PACKED_BYTE_ARRAY: - case Variant::PACKED_INT32_ARRAY: - case Variant::PACKED_INT64_ARRAY: { - result.builtin_type = Variant::INT; - } break; - // Return real - case Variant::PACKED_FLOAT32_ARRAY: - case Variant::PACKED_FLOAT64_ARRAY: - case Variant::VECTOR2: - case Variant::VECTOR3: - case Variant::QUAT: { - result.builtin_type = Variant::FLOAT; - } break; - // Return color - case Variant::PACKED_COLOR_ARRAY: { - result.builtin_type = Variant::COLOR; - } break; - // Return string - case Variant::PACKED_STRING_ARRAY: - case Variant::STRING: { - result.builtin_type = Variant::STRING; - } break; - // Return Vector2 - case Variant::PACKED_VECTOR2_ARRAY: - case Variant::TRANSFORM2D: - case Variant::RECT2: { - result.builtin_type = Variant::VECTOR2; - } break; - // Return Vector3 - case Variant::PACKED_VECTOR3_ARRAY: - case Variant::AABB: - case Variant::BASIS: { - result.builtin_type = Variant::VECTOR3; - } break; - // Depends on the index - case Variant::TRANSFORM: - case Variant::PLANE: - case Variant::COLOR: - default: { - result.has_type = false; - } break; - } - node_type = result; - } - } break; - default: { - _set_error("Parser bug: unhandled operation.", op->line); - ERR_FAIL_V(DataType()); - } + if (key != nullptr && value != nullptr) { + dictionary->elements.push_back({ key, value }); } - } break; - default: { - } + } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); } + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" after dictionary elements.)"); - node_type = _resolve_type(node_type, p_node->line); - p_node->set_datatype(node_type); - return node_type; + return dictionary; } -bool GDScriptParser::_get_function_signature(DataType &p_base_type, const StringName &p_function, DataType &r_return_type, List<DataType> &r_arg_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) const { - r_static = false; - r_default_arg_count = 0; - - DataType original_type = p_base_type; - ClassNode *base = nullptr; - FunctionNode *callee = nullptr; - - if (p_base_type.kind == DataType::CLASS) { - base = p_base_type.class_type; +GDScriptParser::ExpressionNode *GDScriptParser::parse_grouping(ExpressionNode *p_previous_operand, bool p_can_assign) { + ExpressionNode *grouped = parse_expression(false); + pop_multiline(); + if (grouped == nullptr) { + push_error(R"(Expected grouping expression.)"); + } else { + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after grouping expression.)*"); } + return grouped; +} - // Look up the current file (parse tree) - while (!callee && base) { - for (int i = 0; i < base->static_functions.size(); i++) { - FunctionNode *func = base->static_functions[i]; - if (p_function == func->name) { - r_static = true; - callee = func; - break; - } - } - if (!callee && !p_base_type.is_meta_type) { - for (int i = 0; i < base->functions.size(); i++) { - FunctionNode *func = base->functions[i]; - if (p_function == func->name) { - callee = func; - break; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_attribute(ExpressionNode *p_previous_operand, bool p_can_assign) { + SubscriptNode *attribute = alloc_node<SubscriptNode>(); + + if (for_completion) { + bool is_builtin = false; + if (p_previous_operand->type == Node::IDENTIFIER) { + const IdentifierNode *id = static_cast<const IdentifierNode *>(p_previous_operand); + Variant::Type builtin_type = get_builtin_type(id->name); + if (builtin_type < Variant::VARIANT_MAX) { + make_completion_context(COMPLETION_BUILT_IN_TYPE_CONSTANT, builtin_type, true); + is_builtin = true; } } - p_base_type = base->base_type; - if (p_base_type.kind == DataType::CLASS) { - base = p_base_type.class_type; - } else { - break; + if (!is_builtin) { + make_completion_context(COMPLETION_ATTRIBUTE, attribute, -1, true); } } - if (callee) { - r_return_type = callee->get_datatype(); - for (int i = 0; i < callee->argument_types.size(); i++) { - r_arg_types.push_back(callee->argument_types[i]); - } - r_default_arg_count = callee->default_values.size(); - return true; - } + attribute->is_attribute = true; + attribute->base = p_previous_operand; - // Nothing in current file, check parent script - Ref<GDScript> base_gdscript; - Ref<Script> base_script; - StringName native; - if (p_base_type.kind == DataType::GDSCRIPT) { - base_gdscript = p_base_type.script_type; - if (base_gdscript.is_null() || !base_gdscript->is_valid()) { - // GDScript wasn't properly compíled, don't bother trying - return false; - } - } else if (p_base_type.kind == DataType::SCRIPT) { - base_script = p_base_type.script_type; - } else if (p_base_type.kind == DataType::NATIVE) { - native = p_base_type.native_type; + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier after "." for attribute access.)")) { + return attribute; } + attribute->attribute = parse_identifier(); - while (base_gdscript.is_valid()) { - native = base_gdscript->get_instance_base_type(); + return attribute; +} - Map<StringName, GDScriptFunction *> funcs = base_gdscript->get_member_functions(); +GDScriptParser::ExpressionNode *GDScriptParser::parse_subscript(ExpressionNode *p_previous_operand, bool p_can_assign) { + SubscriptNode *subscript = alloc_node<SubscriptNode>(); - if (funcs.has(p_function)) { - GDScriptFunction *f = funcs[p_function]; - r_static = f->is_static(); - r_default_arg_count = f->get_default_argument_count(); - r_return_type = _type_from_gdtype(f->get_return_type()); - for (int i = 0; i < f->get_argument_count(); i++) { - r_arg_types.push_back(_type_from_gdtype(f->get_argument_type(i))); - } - return true; - } + make_completion_context(COMPLETION_SUBSCRIPT, subscript); - base_gdscript = base_gdscript->get_base_script(); - } + subscript->base = p_previous_operand; + subscript->index = parse_expression(false); - while (base_script.is_valid()) { - native = base_script->get_instance_base_type(); - MethodInfo mi = base_script->get_method_info(p_function); + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected "]" after subscription index.)"); - if (!(mi == MethodInfo())) { - r_return_type = _type_from_property(mi.return_val, false); - r_default_arg_count = mi.default_arguments.size(); - for (List<PropertyInfo>::Element *E = mi.arguments.front(); E; E = E->next()) { - r_arg_types.push_back(_type_from_property(E->get())); - } - return true; - } - base_script = base_script->get_base_script(); - } + return subscript; +} - if (native == StringName()) { - // Empty native class, might happen in some Script implementations - // Just ignore it - return false; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_cast(ExpressionNode *p_previous_operand, bool p_can_assign) { + CastNode *cast = alloc_node<CastNode>(); -#ifdef DEBUG_METHODS_ENABLED + cast->operand = p_previous_operand; + cast->cast_type = parse_type(); - // Only native remains - if (!ClassDB::class_exists(native)) { - native = "_" + native.operator String(); - } - if (!ClassDB::class_exists(native)) { - if (!check_types) { - return false; - } - ERR_FAIL_V_MSG(false, "Parser bug: Class '" + String(native) + "' not found."); + if (cast->cast_type == nullptr) { + push_error(R"(Expected type specifier after "as".)"); + return p_previous_operand; } - MethodBind *method = ClassDB::get_method(native, p_function); + return cast; +} - if (!method) { - // Try virtual methods - List<MethodInfo> virtuals; - ClassDB::get_virtual_methods(native, &virtuals); +GDScriptParser::ExpressionNode *GDScriptParser::parse_call(ExpressionNode *p_previous_operand, bool p_can_assign) { + CallNode *call = alloc_node<CallNode>(); - for (const List<MethodInfo>::Element *E = virtuals.front(); E; E = E->next()) { - const MethodInfo &mi = E->get(); - if (mi.name == p_function) { - r_default_arg_count = mi.default_arguments.size(); - for (const List<PropertyInfo>::Element *pi = mi.arguments.front(); pi; pi = pi->next()) { - r_arg_types.push_back(_type_from_property(pi->get())); - } - r_return_type = _type_from_property(mi.return_val, false); - r_vararg = mi.flags & METHOD_FLAG_VARARG; - return true; + if (previous.type == GDScriptTokenizer::Token::SUPER) { + // Super call. + call->is_super = true; + push_multiline(true); + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + // Implicit call to the parent method of the same name. + if (current_function == nullptr) { + push_error(R"(Cannot use implicit "super" call outside of a function.)"); + pop_multiline(); + return nullptr; } + call->function_name = current_function->identifier->name; + } else { + consume(GDScriptTokenizer::Token::PERIOD, R"(Expected "." or "(" after "super".)"); + make_completion_context(COMPLETION_SUPER_METHOD, call, true); + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after ".".)")) { + pop_multiline(); + return nullptr; + } + IdentifierNode *identifier = parse_identifier(); + call->callee = identifier; + call->function_name = identifier->name; + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after function name.)"); } - - // If the base is a script, it might be trying to access members of the Script class itself - if (original_type.is_meta_type && !(p_function == "new") && (original_type.kind == DataType::SCRIPT || original_type.kind == DataType::GDSCRIPT)) { - method = ClassDB::get_method(original_type.script_type->get_class_name(), p_function); - - if (method) { - r_static = true; + } else { + call->callee = p_previous_operand; + + if (call->callee == nullptr) { + push_error(R"*(Cannot call on an expression. Use ".call()" if it's a Callable.)*"); + } else if (call->callee->type == Node::IDENTIFIER) { + call->function_name = static_cast<IdentifierNode *>(call->callee)->name; + make_completion_context(COMPLETION_METHOD, call->callee); + } else if (call->callee->type == Node::SUBSCRIPT) { + SubscriptNode *attribute = static_cast<SubscriptNode *>(call->callee); + if (attribute->is_attribute) { + if (attribute->attribute) { + call->function_name = attribute->attribute->name; + } + make_completion_context(COMPLETION_ATTRIBUTE_METHOD, call->callee); } else { - // Try virtual methods of the script type - virtuals.clear(); - ClassDB::get_virtual_methods(original_type.script_type->get_class_name(), &virtuals); - for (const List<MethodInfo>::Element *E = virtuals.front(); E; E = E->next()) { - const MethodInfo &mi = E->get(); - if (mi.name == p_function) { - r_default_arg_count = mi.default_arguments.size(); - for (const List<PropertyInfo>::Element *pi = mi.arguments.front(); pi; pi = pi->next()) { - r_arg_types.push_back(_type_from_property(pi->get())); - } - r_return_type = _type_from_property(mi.return_val, false); - r_static = true; - r_vararg = mi.flags & METHOD_FLAG_VARARG; - return true; - } - } - return false; + // TODO: The analyzer can see if this is actually a Callable and give better error message. + push_error(R"*(Cannot call on an expression. Use ".call()" if it's a Callable.)*"); } } else { - return false; + push_error(R"*(Cannot call on an expression. Use ".call()" if it's a Callable.)*"); } } - r_default_arg_count = method->get_default_argument_count(); - r_return_type = _type_from_property(method->get_return_info(), false); - r_vararg = method->is_vararg(); - - for (int i = 0; i < method->get_argument_count(); i++) { - r_arg_types.push_back(_type_from_property(method->get_argument_info(i))); - } - return true; -#else - return false; -#endif -} - -GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const OperatorNode *p_call) { - if (p_call->arguments.size() < 1) { - _set_error("Parser bug: function call without enough arguments.", p_call->line); - ERR_FAIL_V(DataType()); - } - - DataType return_type; - List<DataType> arg_types; - int default_args_count = 0; - int arg_count = p_call->arguments.size(); - String callee_name; - bool is_vararg = false; - - switch (p_call->arguments[0]->type) { - case GDScriptParser::Node::TYPE_TYPE: { - // Built-in constructor, special case - TypeNode *tn = static_cast<TypeNode *>(p_call->arguments[0]); - - Vector<DataType> par_types; - par_types.resize(p_call->arguments.size() - 1); - for (int i = 1; i < p_call->arguments.size(); i++) { - par_types.write[i - 1] = _reduce_node_type(p_call->arguments[i]); - } - - if (error_set) { - return DataType(); + if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Arguments. + push_completion_call(call); + make_completion_context(COMPLETION_CALL_ARGUMENTS, call, 0, true); + int argument_index = 0; + do { + make_completion_context(COMPLETION_CALL_ARGUMENTS, call, argument_index++, true); + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; } - - // Special case: check copy constructor. Those are defined implicitly in Variant. - if (par_types.size() == 1) { - if (!par_types[0].has_type || (par_types[0].kind == DataType::BUILTIN && par_types[0].builtin_type == tn->vtype)) { - DataType result; - result.has_type = true; - result.kind = DataType::BUILTIN; - result.builtin_type = tn->vtype; - return result; - } + ExpressionNode *argument = parse_expression(false); + if (argument == nullptr) { + push_error(R"(Expected expression as the function argument.)"); + } else { + call->arguments.push_back(argument); } + } while (match(GDScriptTokenizer::Token::COMMA)); + pop_completion_call(); + } - bool match = false; - List<MethodInfo> constructors; - Variant::get_constructor_list(tn->vtype, &constructors); - PropertyInfo return_type2; - - for (List<MethodInfo>::Element *E = constructors.front(); E; E = E->next()) { - MethodInfo &mi = E->get(); - - if (p_call->arguments.size() - 1 < mi.arguments.size() - mi.default_arguments.size()) { - continue; - } - if (p_call->arguments.size() - 1 > mi.arguments.size()) { - continue; - } - - bool types_match = true; - for (int i = 0; i < par_types.size(); i++) { - DataType arg_type; - if (mi.arguments[i].type != Variant::NIL) { - arg_type.has_type = true; - arg_type.kind = mi.arguments[i].type == Variant::OBJECT ? DataType::NATIVE : DataType::BUILTIN; - arg_type.builtin_type = mi.arguments[i].type; - arg_type.native_type = mi.arguments[i].class_name; - } + pop_multiline(); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after call arguments.)*"); - if (!_is_type_compatible(arg_type, par_types[i], true)) { - types_match = false; - break; - } else { -#ifdef DEBUG_ENABLED - if (arg_type.kind == DataType::BUILTIN && arg_type.builtin_type == Variant::INT && par_types[i].kind == DataType::BUILTIN && par_types[i].builtin_type == Variant::FLOAT) { - _add_warning(GDScriptWarning::NARROWING_CONVERSION, p_call->line, Variant::get_type_name(tn->vtype)); - } - if (par_types[i].may_yield && p_call->arguments[i + 1]->type == Node::TYPE_OPERATOR) { - _add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, p_call->line, _find_function_name(static_cast<OperatorNode *>(p_call->arguments[i + 1]))); - } -#endif // DEBUG_ENABLED - } - } + return call; +} - if (types_match) { - match = true; - return_type2 = mi.return_val; - break; - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p_previous_operand, bool p_can_assign) { + if (match(GDScriptTokenizer::Token::LITERAL)) { + if (previous.literal.get_type() != Variant::STRING) { + push_error(R"(Expect node path as string or identifer after "$".)"); + return nullptr; + } + GetNodeNode *get_node = alloc_node<GetNodeNode>(); + make_completion_context(COMPLETION_GET_NODE, get_node); + get_node->string = parse_literal(); + return get_node; + } else if (current.is_node_name()) { + GetNodeNode *get_node = alloc_node<GetNodeNode>(); + int chain_position = 0; + do { + make_completion_context(COMPLETION_GET_NODE, get_node, chain_position++); + if (!current.is_node_name()) { + push_error(R"(Expect node path after "/".)"); + return nullptr; } + advance(); + IdentifierNode *identifier = alloc_node<IdentifierNode>(); + identifier->name = previous.get_identifier(); + get_node->chain.push_back(identifier); + } while (match(GDScriptTokenizer::Token::SLASH)); + return get_node; + } else { + push_error(R"(Expect node path as string or identifer after "$".)"); + return nullptr; + } +} - if (match) { - return _type_from_property(return_type2, false); - } else if (check_types) { - String err = "No constructor of '"; - err += Variant::get_type_name(tn->vtype); - err += "' matches the signature '"; - err += Variant::get_type_name(tn->vtype) + "("; - for (int i = 0; i < par_types.size(); i++) { - if (i > 0) { - err += ", "; - } - err += par_types[i].to_string(); - } - err += ")'."; - _set_error(err, p_call->line, p_call->column); - return DataType(); - } - return DataType(); - } break; - case GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION: { - BuiltInFunctionNode *func = static_cast<BuiltInFunctionNode *>(p_call->arguments[0]); - MethodInfo mi = GDScriptFunctions::get_info(func->function); +GDScriptParser::ExpressionNode *GDScriptParser::parse_preload(ExpressionNode *p_previous_operand, bool p_can_assign) { + PreloadNode *preload = alloc_node<PreloadNode>(); + preload->resolved_path = "<missing path>"; - return_type = _type_from_property(mi.return_val, false); + push_multiline(true); + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "preload".)"); - // Check all arguments beforehand to solve warnings - for (int i = 1; i < p_call->arguments.size(); i++) { - _reduce_node_type(p_call->arguments[i]); - } + make_completion_context(COMPLETION_RESOURCE_PATH, preload); + push_completion_call(preload); - // Check arguments + preload->path = parse_expression(false); - is_vararg = mi.flags & METHOD_FLAG_VARARG; + if (preload->path == nullptr) { + push_error(R"(Expected resource path after "(".)"); + } - default_args_count = mi.default_arguments.size(); - callee_name = mi.name; - arg_count -= 1; + pop_completion_call(); - // Check each argument type - for (List<PropertyInfo>::Element *E = mi.arguments.front(); E; E = E->next()) { - arg_types.push_back(_type_from_property(E->get())); - } - } break; - default: { - if (p_call->op == OperatorNode::OP_CALL && p_call->arguments.size() < 2) { - _set_error("Parser bug: self method call without enough arguments.", p_call->line); - ERR_FAIL_V(DataType()); - } + pop_multiline(); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after preload path.)*"); - int arg_id = p_call->op == OperatorNode::OP_CALL ? 1 : 0; + return preload; +} - if (p_call->arguments[arg_id]->type != Node::TYPE_IDENTIFIER) { - _set_error("Parser bug: invalid function call argument.", p_call->line); - ERR_FAIL_V(DataType()); - } +GDScriptParser::ExpressionNode *GDScriptParser::parse_invalid_token(ExpressionNode *p_previous_operand, bool p_can_assign) { + // Just for better error messages. + GDScriptTokenizer::Token::Type invalid = previous.type; - // Check all arguments beforehand to solve warnings - for (int i = arg_id + 1; i < p_call->arguments.size(); i++) { - _reduce_node_type(p_call->arguments[i]); - } + switch (invalid) { + case GDScriptTokenizer::Token::QUESTION_MARK: + push_error(R"(Unexpected "?" in source. If you want a ternary operator, use "truthy_value if true_condition else falsy_value".)"); + break; + default: + return nullptr; // Unreachable. + } - IdentifierNode *func_id = static_cast<IdentifierNode *>(p_call->arguments[arg_id]); - callee_name = func_id->name; - arg_count -= 1 + arg_id; + // Return the previous expression. + return p_previous_operand; +} - DataType base_type; - if (p_call->op == OperatorNode::OP_PARENT_CALL) { - base_type = current_class->base_type; +GDScriptParser::TypeNode *GDScriptParser::parse_type(bool p_allow_void) { + TypeNode *type = alloc_node<TypeNode>(); + make_completion_context(p_allow_void ? COMPLETION_TYPE_NAME_OR_VOID : COMPLETION_TYPE_NAME, type); + if (!match(GDScriptTokenizer::Token::IDENTIFIER)) { + if (match(GDScriptTokenizer::Token::VOID)) { + if (p_allow_void) { + TypeNode *void_type = alloc_node<TypeNode>(); + return void_type; } else { - base_type = _reduce_node_type(p_call->arguments[0]); + push_error(R"("void" is only allowed for a function return type.)"); } + } + // Leave error message to the caller who knows the context. + return nullptr; + } - if (!base_type.has_type || (base_type.kind == DataType::BUILTIN && base_type.builtin_type == Variant::NIL)) { - _mark_line_as_unsafe(p_call->line); - return DataType(); - } - - if (base_type.kind == DataType::BUILTIN) { - Callable::CallError err; - Variant tmp = Variant::construct(base_type.builtin_type, nullptr, 0, err); - - if (check_types) { - if (!tmp.has_method(callee_name)) { - _set_error("The method \"" + callee_name + "\" isn't declared on base \"" + base_type.to_string() + "\".", p_call->line); - return DataType(); - } - - default_args_count = Variant::get_method_default_arguments(base_type.builtin_type, callee_name).size(); - const Vector<Variant::Type> &var_arg_types = Variant::get_method_argument_types(base_type.builtin_type, callee_name); - - for (int i = 0; i < var_arg_types.size(); i++) { - DataType argtype; - if (var_arg_types[i] != Variant::NIL) { - argtype.has_type = true; - argtype.kind = DataType::BUILTIN; - argtype.builtin_type = var_arg_types[i]; - } - arg_types.push_back(argtype); - } - } + IdentifierNode *type_element = parse_identifier(); + + type->type_chain.push_back(type_element); + + int chain_index = 1; + while (match(GDScriptTokenizer::Token::PERIOD)) { + make_completion_context(COMPLETION_TYPE_ATTRIBUTE, type, chain_index++); + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected inner type name after ".".)")) { + type_element = parse_identifier(); + type->type_chain.push_back(type_element); + } + } + + return type; +} + +GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Type p_token_type) { + // Function table for expression parsing. + // clang-format destroys the alignment here, so turn off for the table. + /* clang-format off */ + static ParseRule rules[] = { + // PREFIX INFIX PRECEDENCE (for infix) + { nullptr, nullptr, PREC_NONE }, // EMPTY, + // Basic + { nullptr, nullptr, PREC_NONE }, // ANNOTATION, + { &GDScriptParser::parse_identifier, nullptr, PREC_NONE }, // IDENTIFIER, + { &GDScriptParser::parse_literal, nullptr, PREC_NONE }, // LITERAL, + // Comparison + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // LESS, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // LESS_EQUAL, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // GREATER, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // GREATER_EQUAL, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // EQUAL_EQUAL, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_COMPARISON }, // BANG_EQUAL, + // Logical + { nullptr, &GDScriptParser::parse_binary_operator, PREC_LOGIC_AND }, // AND, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_LOGIC_OR }, // OR, + { &GDScriptParser::parse_unary_operator, nullptr, PREC_NONE }, // NOT, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_LOGIC_AND }, // AMPERSAND_AMPERSAND, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_LOGIC_OR }, // PIPE_PIPE, + { &GDScriptParser::parse_unary_operator, nullptr, PREC_NONE }, // BANG, + // Bitwise + { nullptr, &GDScriptParser::parse_binary_operator, PREC_BIT_AND }, // AMPERSAND, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_BIT_OR }, // PIPE, + { &GDScriptParser::parse_unary_operator, nullptr, PREC_NONE }, // TILDE, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_BIT_XOR }, // CARET, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_BIT_SHIFT }, // LESS_LESS, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_BIT_SHIFT }, // GREATER_GREATER, + // Math + { &GDScriptParser::parse_unary_operator, &GDScriptParser::parse_binary_operator, PREC_ADDITION }, // PLUS, + { &GDScriptParser::parse_unary_operator, &GDScriptParser::parse_binary_operator, PREC_SUBTRACTION }, // MINUS, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_FACTOR }, // STAR, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_FACTOR }, // SLASH, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_FACTOR }, // PERCENT, + // Assignment + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // PLUS_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // MINUS_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // STAR_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // SLASH_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // PERCENT_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // LESS_LESS_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // GREATER_GREATER_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // AMPERSAND_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // PIPE_EQUAL, + { nullptr, &GDScriptParser::parse_assignment, PREC_ASSIGNMENT }, // CARET_EQUAL, + // Control flow + { nullptr, &GDScriptParser::parse_ternary_operator, PREC_TERNARY }, // IF, + { nullptr, nullptr, PREC_NONE }, // ELIF, + { nullptr, nullptr, PREC_NONE }, // ELSE, + { nullptr, nullptr, PREC_NONE }, // FOR, + { nullptr, nullptr, PREC_NONE }, // WHILE, + { nullptr, nullptr, PREC_NONE }, // BREAK, + { nullptr, nullptr, PREC_NONE }, // CONTINUE, + { nullptr, nullptr, PREC_NONE }, // PASS, + { nullptr, nullptr, PREC_NONE }, // RETURN, + { nullptr, nullptr, PREC_NONE }, // MATCH, + // Keywords + { nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS, + { nullptr, nullptr, PREC_NONE }, // ASSERT, + { &GDScriptParser::parse_await, nullptr, PREC_NONE }, // AWAIT, + { nullptr, nullptr, PREC_NONE }, // BREAKPOINT, + { nullptr, nullptr, PREC_NONE }, // CLASS, + { nullptr, nullptr, PREC_NONE }, // CLASS_NAME, + { nullptr, nullptr, PREC_NONE }, // CONST, + { nullptr, nullptr, PREC_NONE }, // ENUM, + { nullptr, nullptr, PREC_NONE }, // EXTENDS, + { nullptr, nullptr, PREC_NONE }, // FUNC, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_CONTENT_TEST }, // IN, + { nullptr, &GDScriptParser::parse_binary_operator, PREC_TYPE_TEST }, // IS, + { nullptr, nullptr, PREC_NONE }, // NAMESPACE, + { &GDScriptParser::parse_preload, nullptr, PREC_NONE }, // PRELOAD, + { &GDScriptParser::parse_self, nullptr, PREC_NONE }, // SELF, + { nullptr, nullptr, PREC_NONE }, // SIGNAL, + { nullptr, nullptr, PREC_NONE }, // STATIC, + { &GDScriptParser::parse_call, nullptr, PREC_NONE }, // SUPER, + { nullptr, nullptr, PREC_NONE }, // TRAIT, + { nullptr, nullptr, PREC_NONE }, // VAR, + { nullptr, nullptr, PREC_NONE }, // VOID, + { nullptr, nullptr, PREC_NONE }, // YIELD, + // Punctuation + { &GDScriptParser::parse_array, &GDScriptParser::parse_subscript, PREC_SUBSCRIPT }, // BRACKET_OPEN, + { nullptr, nullptr, PREC_NONE }, // BRACKET_CLOSE, + { &GDScriptParser::parse_dictionary, nullptr, PREC_NONE }, // BRACE_OPEN, + { nullptr, nullptr, PREC_NONE }, // BRACE_CLOSE, + { &GDScriptParser::parse_grouping, &GDScriptParser::parse_call, PREC_CALL }, // PARENTHESIS_OPEN, + { nullptr, nullptr, PREC_NONE }, // PARENTHESIS_CLOSE, + { nullptr, nullptr, PREC_NONE }, // COMMA, + { nullptr, nullptr, PREC_NONE }, // SEMICOLON, + { nullptr, &GDScriptParser::parse_attribute, PREC_ATTRIBUTE }, // PERIOD, + { nullptr, nullptr, PREC_NONE }, // PERIOD_PERIOD, + { nullptr, nullptr, PREC_NONE }, // COLON, + { &GDScriptParser::parse_get_node, nullptr, PREC_NONE }, // DOLLAR, + { nullptr, nullptr, PREC_NONE }, // FORWARD_ARROW, + { nullptr, nullptr, PREC_NONE }, // UNDERSCORE, + // Whitespace + { nullptr, nullptr, PREC_NONE }, // NEWLINE, + { nullptr, nullptr, PREC_NONE }, // INDENT, + { nullptr, nullptr, PREC_NONE }, // DEDENT, + // Constants + { &GDScriptParser::parse_builtin_constant, nullptr, PREC_NONE }, // CONST_PI, + { &GDScriptParser::parse_builtin_constant, nullptr, PREC_NONE }, // CONST_TAU, + { &GDScriptParser::parse_builtin_constant, nullptr, PREC_NONE }, // CONST_INF, + { &GDScriptParser::parse_builtin_constant, nullptr, PREC_NONE }, // CONST_NAN, + // Error message improvement + { nullptr, nullptr, PREC_NONE }, // VCS_CONFLICT_MARKER, + { nullptr, nullptr, PREC_NONE }, // BACKTICK, + { nullptr, &GDScriptParser::parse_invalid_token, PREC_CAST }, // QUESTION_MARK, + // Special + { nullptr, nullptr, PREC_NONE }, // ERROR, + { nullptr, nullptr, PREC_NONE }, // TK_EOF, + }; + /* clang-format on */ + // Avoid desync. + static_assert(sizeof(rules) / sizeof(rules[0]) == GDScriptTokenizer::Token::TK_MAX, "Amount of parse rules don't match the amount of token types."); + + // Let's assume this this never invalid, since nothing generates a TK_MAX. + return &rules[p_token_type]; +} + +bool GDScriptParser::SuiteNode::has_local(const StringName &p_name) const { + if (locals_indices.has(p_name)) { + return true; + } + if (parent_block != nullptr) { + return parent_block->has_local(p_name); + } + return false; +} - bool rets = false; - return_type.has_type = true; - return_type.kind = DataType::BUILTIN; - return_type.builtin_type = Variant::get_method_return_type(base_type.builtin_type, callee_name, &rets); - // If the method returns, but it might return any type, (Variant::NIL), pretend we don't know the type. - // At least make sure we know that it returns - if (rets && return_type.builtin_type == Variant::NIL) { - return_type.has_type = false; - } - break; - } +const GDScriptParser::SuiteNode::Local &GDScriptParser::SuiteNode::get_local(const StringName &p_name) const { + if (locals_indices.has(p_name)) { + return locals[locals_indices[p_name]]; + } + if (parent_block != nullptr) { + return parent_block->get_local(p_name); + } + return empty; +} - DataType original_type = base_type; - bool is_initializer = callee_name == "new"; - bool is_static = false; - bool valid = false; +bool GDScriptParser::AnnotationNode::apply(GDScriptParser *p_this, Node *p_target) const { + return (p_this->*(p_this->valid_annotations[name].apply))(this, p_target); +} - if (is_initializer && original_type.is_meta_type) { - // Try to check it as initializer - base_type = original_type; - callee_name = "_init"; - base_type.is_meta_type = false; +bool GDScriptParser::AnnotationNode::applies_to(uint32_t p_target_kinds) const { + return (info->target_kind & p_target_kinds) > 0; +} - valid = _get_function_signature(base_type, callee_name, return_type, arg_types, - default_args_count, is_static, is_vararg); +bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) { + ERR_FAIL_COND_V_MSG(!valid_annotations.has(p_annotation->name), false, vformat(R"(Annotation "%s" not found to validate.)", p_annotation->name)); - return_type = original_type; - return_type.is_meta_type = false; + const MethodInfo &info = valid_annotations[p_annotation->name].info; - valid = true; // There's always an initializer, we can assume this is true - } + if (((info.flags & METHOD_FLAG_VARARG) == 0) && p_annotation->arguments.size() > info.arguments.size()) { + push_error(vformat(R"(Annotation "%s" requires at most %d arguments, but %d were given.)", p_annotation->name, info.arguments.size(), p_annotation->arguments.size())); + return false; + } - if (!valid) { - base_type = original_type; - return_type = DataType(); - valid = _get_function_signature(base_type, callee_name, return_type, arg_types, - default_args_count, is_static, is_vararg); - } + if (p_annotation->arguments.size() < info.arguments.size() - info.default_arguments.size()) { + push_error(vformat(R"(Annotation "%s" requires at least %d arguments, but %d were given.)", p_annotation->name, info.arguments.size() - info.default_arguments.size(), p_annotation->arguments.size())); + return false; + } - if (!valid) { -#ifdef DEBUG_ENABLED - if (p_call->arguments[0]->type == Node::TYPE_SELF) { - _set_error("The method \"" + callee_name + "\" isn't declared in the current class.", p_call->line); - return DataType(); - } - DataType tmp_type; - valid = _get_member_type(original_type, func_id->name, tmp_type); - if (valid) { - if (tmp_type.is_constant) { - _add_warning(GDScriptWarning::CONSTANT_USED_AS_FUNCTION, p_call->line, callee_name, original_type.to_string()); - } else { - _add_warning(GDScriptWarning::PROPERTY_USED_AS_FUNCTION, p_call->line, callee_name, original_type.to_string()); + const List<PropertyInfo>::Element *E = info.arguments.front(); + for (int i = 0; i < p_annotation->arguments.size(); i++) { + ExpressionNode *argument = p_annotation->arguments[i]; + const PropertyInfo ¶meter = E->get(); + + if (E->next() != nullptr) { + E = E->next(); + } + + switch (parameter.type) { + case Variant::STRING: + case Variant::STRING_NAME: + case Variant::NODE_PATH: + // Allow "quote-less strings", as long as they are recognized as identifiers. + if (argument->type == Node::IDENTIFIER) { + IdentifierNode *string = static_cast<IdentifierNode *>(argument); + Callable::CallError error; + Vector<Variant> args = varray(string->name); + const Variant *name = args.ptr(); + p_annotation->resolved_arguments.push_back(Variant::construct(parameter.type, &(name), 1, error)); + if (error.error != Callable::CallError::CALL_OK) { + push_error(vformat(R"(Expected %s as argument %d of annotation "%s").)", Variant::get_type_name(parameter.type), i + 1, p_annotation->name)); + p_annotation->resolved_arguments.remove(p_annotation->resolved_arguments.size() - 1); + return false; } + break; + } + [[fallthrough]]; + default: { + if (argument->type != Node::LITERAL) { + push_error(vformat(R"(Expected %s as argument %d of annotation "%s").)", Variant::get_type_name(parameter.type), i + 1, p_annotation->name)); + return false; } - _add_warning(GDScriptWarning::UNSAFE_METHOD_ACCESS, p_call->line, callee_name, original_type.to_string()); - _mark_line_as_unsafe(p_call->line); -#endif // DEBUG_ENABLED - return DataType(); - } - -#ifdef DEBUG_ENABLED - if (current_function && !for_completion && !is_static && p_call->arguments[0]->type == Node::TYPE_SELF && current_function->_static) { - _set_error("Can't call non-static function from a static function.", p_call->line); - return DataType(); - } - - if (check_types && !is_static && !is_initializer && base_type.is_meta_type) { - _set_error("Non-static function \"" + String(callee_name) + "\" can only be called from an instance.", p_call->line); - return DataType(); - } - // Check signal emission for warnings - if (callee_name == "emit_signal" && p_call->op == OperatorNode::OP_CALL && p_call->arguments[0]->type == Node::TYPE_SELF && p_call->arguments.size() >= 3 && p_call->arguments[2]->type == Node::TYPE_CONSTANT) { - ConstantNode *sig = static_cast<ConstantNode *>(p_call->arguments[2]); - String emitted = sig->value.get_type() == Variant::STRING ? sig->value.operator String() : ""; - for (int i = 0; i < current_class->_signals.size(); i++) { - if (current_class->_signals[i].name == emitted) { - current_class->_signals.write[i].emissions += 1; - break; - } + Variant value = static_cast<LiteralNode *>(argument)->value; + if (!Variant::can_convert_strict(value.get_type(), parameter.type)) { + push_error(vformat(R"(Expected %s as argument %d of annotation "%s").)", Variant::get_type_name(parameter.type), i + 1, p_annotation->name)); + return false; } + Callable::CallError error; + const Variant *args = &value; + p_annotation->resolved_arguments.push_back(Variant::construct(parameter.type, &(args), 1, error)); + if (error.error != Callable::CallError::CALL_OK) { + push_error(vformat(R"(Expected %s as argument %d of annotation "%s").)", Variant::get_type_name(parameter.type), i + 1, p_annotation->name)); + p_annotation->resolved_arguments.remove(p_annotation->resolved_arguments.size() - 1); + return false; + } + break; } -#endif // DEBUG_ENABLED - } break; - } - -#ifdef DEBUG_ENABLED - if (!check_types) { - return return_type; + } } - if (arg_count < arg_types.size() - default_args_count) { - _set_error("Too few arguments for \"" + callee_name + "()\" call. Expected at least " + itos(arg_types.size() - default_args_count) + ".", p_call->line); - return return_type; - } - if (!is_vararg && arg_count > arg_types.size()) { - _set_error("Too many arguments for \"" + callee_name + "()\" call. Expected at most " + itos(arg_types.size()) + ".", p_call->line); - return return_type; - } + return true; +} - int arg_diff = p_call->arguments.size() - arg_count; - for (int i = arg_diff; i < p_call->arguments.size(); i++) { - DataType par_type = _reduce_node_type(p_call->arguments[i]); +bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_node) { + this->_is_tool = true; + return true; +} - if ((i - arg_diff) >= arg_types.size()) { - continue; - } +bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p_node) { + ERR_FAIL_COND_V_MSG(p_node->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)"); + ClassNode *p_class = static_cast<ClassNode *>(p_node); + p_class->icon_path = p_annotation->resolved_arguments[0]; + return true; +} - DataType arg_type = arg_types[i - arg_diff]; +bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_node) { + ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); - if (!par_type.has_type) { - _mark_line_as_unsafe(p_call->line); - if (par_type.may_yield && p_call->arguments[i]->type == Node::TYPE_OPERATOR) { - _add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, p_call->line, _find_function_name(static_cast<OperatorNode *>(p_call->arguments[i]))); - } - } else if (!_is_type_compatible(arg_types[i - arg_diff], par_type, true)) { - // Supertypes are acceptable for dynamic compliance - if (!_is_type_compatible(par_type, arg_types[i - arg_diff])) { - _set_error("At \"" + callee_name + "()\" call, argument " + itos(i - arg_diff + 1) + ". The passed argument's type (" + - par_type.to_string() + ") doesn't match the function's expected argument type (" + - arg_types[i - arg_diff].to_string() + ").", - p_call->line); - return DataType(); - } else { - _mark_line_as_unsafe(p_call->line); - } - } else { - if (arg_type.kind == DataType::BUILTIN && arg_type.builtin_type == Variant::INT && par_type.kind == DataType::BUILTIN && par_type.builtin_type == Variant::FLOAT) { - _add_warning(GDScriptWarning::NARROWING_CONVERSION, p_call->line, callee_name); - } - } + VariableNode *variable = static_cast<VariableNode *>(p_node); + if (variable->onready) { + push_error(R"("@onready" annotation can only be used once per variable.)"); + return false; } - -#endif // DEBUG_ENABLED - - return return_type; + variable->onready = true; + current_class->onready_used = true; + return true; } -bool GDScriptParser::_get_member_type(const DataType &p_base_type, const StringName &p_member, DataType &r_member_type, bool *r_is_const) const { - DataType base_type = p_base_type; +template <PropertyHint t_hint, Variant::Type t_type> +bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_node) { + ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); - // Check classes in current file - ClassNode *base = nullptr; - if (base_type.kind == DataType::CLASS) { - base = base_type.class_type; + VariableNode *variable = static_cast<VariableNode *>(p_node); + if (variable->exported) { + push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation); + return false; } - while (base) { - if (base->constant_expressions.has(p_member)) { - if (r_is_const) { - *r_is_const = true; - } - r_member_type = base->constant_expressions[p_member].expression->get_datatype(); - return true; - } + variable->exported = true; + // TODO: Improving setting type, especially for range hints, which can be int or float. + variable->export_info.type = t_type; + variable->export_info.hint = t_hint; - if (!base_type.is_meta_type) { - for (int i = 0; i < base->variables.size(); i++) { - if (base->variables[i].identifier == p_member) { - r_member_type = base->variables[i].data_type; - base->variables.write[i].usages += 1; - return true; - } + if (p_annotation->name == "@export") { + if (variable->datatype_specifier == nullptr) { + if (variable->initializer == nullptr) { + push_error(R"(Cannot use "@export" annotation with variable without type or initializer, since type can't be inferred.)", p_annotation); + return false; } - } else { - for (int i = 0; i < base->subclasses.size(); i++) { - ClassNode *c = base->subclasses[i]; - if (c->name == p_member) { - DataType class_type; - class_type.has_type = true; - class_type.is_constant = true; - class_type.is_meta_type = true; - class_type.kind = DataType::CLASS; - class_type.class_type = c; - r_member_type = class_type; - return true; - } + if (variable->initializer->type != Node::LITERAL) { + push_error(R"(To use "@export" annotation with type-less variable, the default value must be a literal.)", p_annotation); + return false; } - } - - base_type = base->base_type; - if (base_type.kind == DataType::CLASS) { - base = base_type.class_type; - } else { - break; - } + variable->export_info.type = static_cast<LiteralNode *>(variable->initializer)->value.get_type(); + } // else: Actual type will be set by the analyzer, which can infer the proper type. } - Ref<GDScript> gds; - if (base_type.kind == DataType::GDSCRIPT) { - gds = base_type.script_type; - if (gds.is_null() || !gds->is_valid()) { - // GDScript wasn't properly compíled, don't bother trying - return false; + String hint_string; + for (int i = 0; i < p_annotation->resolved_arguments.size(); i++) { + if (i > 0) { + hint_string += ","; } + hint_string += String(p_annotation->resolved_arguments[i]); } - Ref<Script> scr; - if (base_type.kind == DataType::SCRIPT) { - scr = base_type.script_type; - } + variable->export_info.hint_string = hint_string; - StringName native; - if (base_type.kind == DataType::NATIVE) { - native = base_type.native_type; - } - - // Check GDScripts - while (gds.is_valid()) { - if (gds->get_constants().has(p_member)) { - Variant c = gds->get_constants()[p_member]; - r_member_type = _type_from_variant(c); - return true; - } - - if (!base_type.is_meta_type) { - if (gds->get_members().has(p_member)) { - r_member_type = _type_from_gdtype(gds->get_member_type(p_member)); - return true; - } - } - - native = gds->get_instance_base_type(); - if (gds->get_base_script().is_valid()) { - gds = gds->get_base_script(); - scr = gds->get_base_script(); - bool is_meta = base_type.is_meta_type; - base_type = _type_from_variant(scr.operator Variant()); - base_type.is_meta_type = is_meta; - } else { - break; - } - } - -#define IS_USAGE_MEMBER(m_usage) (!(m_usage & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_CATEGORY))) - - // Check other script types - while (scr.is_valid()) { - Map<StringName, Variant> constants; - scr->get_constants(&constants); - if (constants.has(p_member)) { - r_member_type = _type_from_variant(constants[p_member]); - return true; - } - - List<PropertyInfo> properties; - scr->get_script_property_list(&properties); - for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().name == p_member && IS_USAGE_MEMBER(E->get().usage)) { - r_member_type = _type_from_property(E->get()); - return true; - } - } - - base_type = _type_from_variant(scr.operator Variant()); - native = scr->get_instance_base_type(); - scr = scr->get_base_script(); - } - - if (native == StringName()) { - // Empty native class, might happen in some Script implementations - // Just ignore it - return false; - } + return true; +} - // Check ClassDB - if (!ClassDB::class_exists(native)) { - native = "_" + native.operator String(); - } - if (!ClassDB::class_exists(native)) { - if (!check_types) { - return false; - } - ERR_FAIL_V_MSG(false, "Parser bug: Class \"" + String(native) + "\" not found."); - } +bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_node) { + ERR_FAIL_V_MSG(false, "Not implemented."); +} - bool valid = false; - ClassDB::get_integer_constant(native, p_member, &valid); - if (valid) { - DataType ct; - ct.has_type = true; - ct.is_constant = true; - ct.kind = DataType::BUILTIN; - ct.builtin_type = Variant::INT; - r_member_type = ct; - return true; - } +template <MultiplayerAPI::RPCMode t_mode> +bool GDScriptParser::network_annotations(const AnnotationNode *p_annotation, Node *p_node) { + ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE && p_node->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to variables and functions.)", p_annotation->name)); - if (!base_type.is_meta_type) { - List<PropertyInfo> properties; - ClassDB::get_property_list(native, &properties); - for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().name == p_member && IS_USAGE_MEMBER(E->get().usage)) { - // Check if a getter exists - StringName getter_name = ClassDB::get_property_getter(native, p_member); - if (getter_name != StringName()) { - // Use the getter return type -#ifdef DEBUG_METHODS_ENABLED - MethodBind *getter_method = ClassDB::get_method(native, getter_name); - if (getter_method) { - r_member_type = _type_from_property(getter_method->get_return_info()); - } else { - r_member_type = DataType(); - } -#else - r_member_type = DataType(); -#endif - } else { - r_member_type = _type_from_property(E->get()); - } - return true; + switch (p_node->type) { + case Node::VARIABLE: { + VariableNode *variable = static_cast<VariableNode *>(p_node); + if (variable->rpc_mode != MultiplayerAPI::RPC_MODE_DISABLED) { + push_error(R"(RPC annotations can only be used once per variable.)", p_annotation); } + variable->rpc_mode = t_mode; + break; } - } - - // If the base is a script, it might be trying to access members of the Script class itself - if (p_base_type.is_meta_type && (p_base_type.kind == DataType::SCRIPT || p_base_type.kind == DataType::GDSCRIPT)) { - native = p_base_type.script_type->get_class_name(); - ClassDB::get_integer_constant(native, p_member, &valid); - if (valid) { - DataType ct; - ct.has_type = true; - ct.is_constant = true; - ct.kind = DataType::BUILTIN; - ct.builtin_type = Variant::INT; - r_member_type = ct; - return true; - } - - List<PropertyInfo> properties; - ClassDB::get_property_list(native, &properties); - for (List<PropertyInfo>::Element *E = properties.front(); E; E = E->next()) { - if (E->get().name == p_member && IS_USAGE_MEMBER(E->get().usage)) { - // Check if a getter exists - StringName getter_name = ClassDB::get_property_getter(native, p_member); - if (getter_name != StringName()) { - // Use the getter return type -#ifdef DEBUG_METHODS_ENABLED - MethodBind *getter_method = ClassDB::get_method(native, getter_name); - if (getter_method) { - r_member_type = _type_from_property(getter_method->get_return_info()); - } else { - r_member_type = DataType(); - } -#else - r_member_type = DataType(); -#endif - } else { - r_member_type = _type_from_property(E->get()); - } - return true; + case Node::FUNCTION: { + FunctionNode *function = static_cast<FunctionNode *>(p_node); + if (function->rpc_mode != MultiplayerAPI::RPC_MODE_DISABLED) { + push_error(R"(RPC annotations can only be used once per function.)", p_annotation); } + function->rpc_mode = t_mode; + break; } + default: + return false; // Unreachable. } -#undef IS_USAGE_MEMBER - return false; + return true; } -GDScriptParser::DataType GDScriptParser::_reduce_identifier_type(const DataType *p_base_type, const StringName &p_identifier, int p_line, bool p_is_indexing) { - if (p_base_type && !p_base_type->has_type) { - return DataType(); - } - - DataType base_type; - DataType member_type; - - if (!p_base_type) { - base_type.has_type = true; - base_type.is_constant = true; - base_type.kind = DataType::CLASS; - base_type.class_type = current_class; - } else { - base_type = DataType(*p_base_type); - } - - bool is_const = false; - if (_get_member_type(base_type, p_identifier, member_type, &is_const)) { - if (!p_base_type && current_function && current_function->_static && !is_const) { - _set_error("Can't access member variable (\"" + p_identifier.operator String() + "\") from a static function.", p_line); +GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const { + switch (type) { + case CONSTANT: + return constant->get_datatype(); + case VARIABLE: + return variable->get_datatype(); + case PARAMETER: + return parameter->get_datatype(); + case FOR_VARIABLE: + case PATTERN_BIND: + return bind->get_datatype(); + case UNDEFINED: return DataType(); - } - return member_type; } + return DataType(); +} - if (p_is_indexing) { - // Don't look for globals since this is an indexed identifier - return DataType(); +String GDScriptParser::SuiteNode::Local::get_name() const { + String name; + switch (type) { + case SuiteNode::Local::PARAMETER: + name = "parameter"; + break; + case SuiteNode::Local::CONSTANT: + name = "constant"; + break; + case SuiteNode::Local::VARIABLE: + name = "variable"; + break; + case SuiteNode::Local::FOR_VARIABLE: + name = "for loop iterator"; + break; + case SuiteNode::Local::PATTERN_BIND: + name = "pattern_bind"; + break; + case SuiteNode::Local::UNDEFINED: + name = "<undefined>"; + break; } + return name; +} - if (!p_base_type) { - // Possibly this is a global, check before failing - - if (ClassDB::class_exists(p_identifier) || ClassDB::class_exists("_" + p_identifier.operator String())) { - DataType result; - result.has_type = true; - result.is_constant = true; - result.is_meta_type = true; - if (Engine::get_singleton()->has_singleton(p_identifier) || Engine::get_singleton()->has_singleton("_" + p_identifier.operator String())) { - result.is_meta_type = false; +String GDScriptParser::DataType::to_string() const { + switch (kind) { + case VARIANT: + return "Variant"; + case BUILTIN: + if (builtin_type == Variant::NIL) { + return "null"; } - result.kind = DataType::NATIVE; - result.native_type = p_identifier; - return result; - } - - ClassNode *outer_class = current_class; - while (outer_class) { - if (outer_class->name == p_identifier) { - DataType result; - result.has_type = true; - result.is_constant = true; - result.is_meta_type = true; - result.kind = DataType::CLASS; - result.class_type = outer_class; - return result; + return Variant::get_type_name(builtin_type); + case NATIVE: + if (is_meta_type) { + return GDScriptNativeClass::get_class_static(); } - if (outer_class->constant_expressions.has(p_identifier)) { - return outer_class->constant_expressions[p_identifier].type; + return native_type.operator String(); + case CLASS: + if (is_meta_type) { + return GDScript::get_class_static(); } - for (int i = 0; i < outer_class->subclasses.size(); i++) { - if (outer_class->subclasses[i] == current_class) { - continue; - } - if (outer_class->subclasses[i]->name == p_identifier) { - DataType result; - result.has_type = true; - result.is_constant = true; - result.is_meta_type = true; - result.kind = DataType::CLASS; - result.class_type = outer_class->subclasses[i]; - return result; - } + if (class_type->identifier != nullptr) { + return class_type->identifier->name.operator String(); } - outer_class = outer_class->owner; - } - - if (ScriptServer::is_global_class(p_identifier)) { - Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier)); - if (scr.is_valid()) { - DataType result; - result.has_type = true; - result.script_type = scr; - result.is_constant = true; - result.is_meta_type = true; - Ref<GDScript> gds = scr; - if (gds.is_valid()) { - if (!gds->is_valid()) { - _set_error("The class \"" + p_identifier + "\" couldn't be fully loaded (script error or cyclic dependency)."); - return DataType(); - } - result.kind = DataType::GDSCRIPT; - } else { - result.kind = DataType::SCRIPT; - } - return result; + return class_type->fqcn; + case SCRIPT: { + if (is_meta_type) { + return script_type->get_class_name().operator String(); } - _set_error("The class \"" + p_identifier + "\" was found in global scope, but its script couldn't be loaded."); - return DataType(); - } - - if (GDScriptLanguage::get_singleton()->get_global_map().has(p_identifier)) { - int idx = GDScriptLanguage::get_singleton()->get_global_map()[p_identifier]; - Variant g = GDScriptLanguage::get_singleton()->get_global_array()[idx]; - return _type_from_variant(g); - } - - if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(p_identifier)) { - Variant g = GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier]; - return _type_from_variant(g); - } - - // Non-tool singletons aren't loaded, check project settings - List<PropertyInfo> props; - ProjectSettings::get_singleton()->get_property_list(&props); - - for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { - String s = E->get().name; - if (!s.begins_with("autoload/")) { - continue; + String name = script_type->get_name(); + if (!name.empty()) { + return name; } - String name = s.get_slice("/", 1); - if (name == p_identifier) { - String script = ProjectSettings::get_singleton()->get(s); - if (script.begins_with("*")) { - script = script.right(1); - } - if (!script.begins_with("res://")) { - script = "res://" + script; - } - Ref<Script> singleton = ResourceLoader::load(script); - if (singleton.is_valid()) { - DataType result; - result.has_type = true; - result.is_constant = true; - result.script_type = singleton; - - Ref<GDScript> gds = singleton; - if (gds.is_valid()) { - if (!gds->is_valid()) { - _set_error("Couldn't fully load the singleton script \"" + p_identifier + "\" (possible cyclic reference or parse error).", p_line); - return DataType(); - } - result.kind = DataType::GDSCRIPT; - } else { - result.kind = DataType::SCRIPT; - } - } + name = script_path; + if (!name.empty()) { + return name; } + return native_type.operator String(); } - - // This means looking in the current class, which type is always known - _set_error("The identifier \"" + p_identifier.operator String() + "\" isn't declared in the current scope.", p_line); - } - -#ifdef DEBUG_ENABLED - { - DataType tmp_type; - List<DataType> arg_types; - int argcount; - bool _static; - bool vararg; - if (_get_function_signature(base_type, p_identifier, tmp_type, arg_types, argcount, _static, vararg)) { - _add_warning(GDScriptWarning::FUNCTION_USED_AS_PROPERTY, p_line, p_identifier.operator String(), base_type.to_string()); - } + case ENUM: + return enum_type.operator String() + " (enum)"; + case ENUM_VALUE: + return enum_type.operator String() + " (enum value)"; + case UNRESOLVED: + return "<unresolved type>"; } -#endif // DEBUG_ENABLED - _mark_line_as_unsafe(p_line); - return DataType(); + ERR_FAIL_V_MSG("<unresolved type", "Kind set outside the enum range."); } -void GDScriptParser::_check_class_level_types(ClassNode *p_class) { - // Names of internal object properties that we check to avoid overriding them. - // "__meta__" could also be in here, but since it doesn't really affect object metadata, - // it is okay to override it on script. - StringName script_name = CoreStringNames::get_singleton()->_script; - - _mark_line_as_safe(p_class->line); +/*---------- PRETTY PRINT FOR DEBUG ----------*/ - // Constants - for (Map<StringName, ClassNode::Constant>::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { - ClassNode::Constant &c = E->get(); - _mark_line_as_safe(c.expression->line); - DataType cont = _resolve_type(c.type, c.expression->line); - DataType expr = _resolve_type(c.expression->get_datatype(), c.expression->line); - - if (check_types && !_is_type_compatible(cont, expr)) { - _set_error("The constant value type (" + expr.to_string() + ") isn't compatible with declared type (" + cont.to_string() + ").", - c.expression->line); - return; - } - - expr.is_constant = true; - c.type = expr; - c.expression->set_datatype(expr); +#ifdef DEBUG_ENABLED - DataType tmp; - const StringName &constant_name = E->key(); - if (constant_name == script_name || _get_member_type(p_class->base_type, constant_name, tmp)) { - _set_error("The member \"" + String(constant_name) + "\" already exists in a parent class.", c.expression->line); - return; +void GDScriptParser::TreePrinter::increase_indent() { + indent_level++; + indent = ""; + for (int i = 0; i < indent_level * 4; i++) { + if (i % 4 == 0) { + indent += "|"; + } else { + indent += " "; } } +} - // Function declarations - for (int i = 0; i < p_class->static_functions.size(); i++) { - _check_function_types(p_class->static_functions[i]); - if (error_set) { - return; +void GDScriptParser::TreePrinter::decrease_indent() { + indent_level--; + indent = ""; + for (int i = 0; i < indent_level * 4; i++) { + if (i % 4 == 0) { + indent += "|"; + } else { + indent += " "; } } +} - for (int i = 0; i < p_class->functions.size(); i++) { - _check_function_types(p_class->functions[i]); - if (error_set) { - return; - } +void GDScriptParser::TreePrinter::push_line(const String &p_line) { + if (!p_line.empty()) { + push_text(p_line); } + printed += "\n"; + pending_indent = true; +} - // Class variables - for (int i = 0; i < p_class->variables.size(); i++) { - ClassNode::Member &v = p_class->variables.write[i]; - - DataType tmp; - if (v.identifier == script_name || _get_member_type(p_class->base_type, v.identifier, tmp)) { - _set_error("The member \"" + String(v.identifier) + "\" already exists in a parent class.", v.line); - return; - } - - _mark_line_as_safe(v.line); - v.data_type = _resolve_type(v.data_type, v.line); - v.initial_assignment->arguments[0]->set_datatype(v.data_type); - - if (v.expression) { - DataType expr_type = _reduce_node_type(v.expression); - - if (check_types && !_is_type_compatible(v.data_type, expr_type)) { - // Try supertype test - if (_is_type_compatible(expr_type, v.data_type)) { - _mark_line_as_unsafe(v.line); - } else { - // Try with implicit conversion - if (v.data_type.kind != DataType::BUILTIN || !_is_type_compatible(v.data_type, expr_type, true)) { - _set_error("The assigned expression's type (" + expr_type.to_string() + ") doesn't match the variable's type (" + - v.data_type.to_string() + ").", - v.line); - return; - } - - // Replace assignment with implicit conversion - BuiltInFunctionNode *convert = alloc_node<BuiltInFunctionNode>(); - convert->line = v.line; - convert->function = GDScriptFunctions::TYPE_CONVERT; - - ConstantNode *tgt_type = alloc_node<ConstantNode>(); - tgt_type->line = v.line; - tgt_type->value = (int64_t)v.data_type.builtin_type; - - OperatorNode *convert_call = alloc_node<OperatorNode>(); - convert_call->line = v.line; - convert_call->op = OperatorNode::OP_CALL; - convert_call->arguments.push_back(convert); - convert_call->arguments.push_back(v.expression); - convert_call->arguments.push_back(tgt_type); - - v.expression = convert_call; - v.initial_assignment->arguments.write[1] = convert_call; - } - } - - if (v.data_type.infer_type) { - if (!expr_type.has_type) { - _set_error("The assigned value doesn't have a set type; the variable type can't be inferred.", v.line); - return; - } - if (expr_type.kind == DataType::BUILTIN && expr_type.builtin_type == Variant::NIL) { - _set_error("The variable type cannot be inferred because its value is \"null\".", v.line); - return; - } - v.data_type = expr_type; - v.data_type.is_constant = false; - } - } - - // Check export hint - if (v.data_type.has_type && v._export.type != Variant::NIL) { - DataType export_type = _type_from_property(v._export); - if (!_is_type_compatible(v.data_type, export_type, true)) { - _set_error("The export hint's type (" + export_type.to_string() + ") doesn't match the variable's type (" + - v.data_type.to_string() + ").", - v.line); - return; - } - } - - // Setter and getter - if (v.setter == StringName() && v.getter == StringName()) { - continue; - } +void GDScriptParser::TreePrinter::push_text(const String &p_text) { + if (pending_indent) { + printed += indent; + pending_indent = false; + } + printed += p_text; +} - bool found_getter = false; - bool found_setter = false; - for (int j = 0; j < p_class->functions.size(); j++) { - if (v.setter == p_class->functions[j]->name) { - found_setter = true; - FunctionNode *setter = p_class->functions[j]; - - if (setter->get_required_argument_count() != 1 && - !(setter->get_required_argument_count() == 0 && setter->default_values.size() > 0)) { - _set_error("The setter function needs to receive exactly 1 argument. See \"" + setter->name + - "()\" definition at line " + itos(setter->line) + ".", - v.line); - return; - } - if (!_is_type_compatible(v.data_type, setter->argument_types[0])) { - _set_error("The setter argument's type (" + setter->argument_types[0].to_string() + - ") doesn't match the variable's type (" + v.data_type.to_string() + "). See \"" + - setter->name + "()\" definition at line " + itos(setter->line) + ".", - v.line); - return; - } - continue; - } - if (v.getter == p_class->functions[j]->name) { - found_getter = true; - FunctionNode *getter = p_class->functions[j]; - - if (getter->get_required_argument_count() != 0) { - _set_error("The getter function can't receive arguments. See \"" + getter->name + - "()\" definition at line " + itos(getter->line) + ".", - v.line); - return; - } - if (!_is_type_compatible(v.data_type, getter->get_datatype())) { - _set_error("The getter return type (" + getter->get_datatype().to_string() + - ") doesn't match the variable's type (" + v.data_type.to_string() + - "). See \"" + getter->name + "()\" definition at line " + itos(getter->line) + ".", - v.line); - return; - } - } - if (found_getter && found_setter) { - break; - } +void GDScriptParser::TreePrinter::print_annotation(AnnotationNode *p_annotation) { + push_text(p_annotation->name); + push_text(" ("); + for (int i = 0; i < p_annotation->arguments.size(); i++) { + if (i > 0) { + push_text(" , "); } + print_expression(p_annotation->arguments[i]); + } + push_line(")"); +} - if ((found_getter || v.getter == StringName()) && (found_setter || v.setter == StringName())) { - continue; +void GDScriptParser::TreePrinter::print_array(ArrayNode *p_array) { + push_text("[ "); + for (int i = 0; i < p_array->elements.size(); i++) { + if (i > 0) { + push_text(" , "); } + print_expression(p_array->elements[i]); + } + push_text(" ]"); +} - // Check for static functions - for (int j = 0; j < p_class->static_functions.size(); j++) { - if (v.setter == p_class->static_functions[j]->name) { - FunctionNode *setter = p_class->static_functions[j]; - _set_error("The setter can't be a static function. See \"" + setter->name + "()\" definition at line " + itos(setter->line) + ".", v.line); - return; - } - if (v.getter == p_class->static_functions[j]->name) { - FunctionNode *getter = p_class->static_functions[j]; - _set_error("The getter can't be a static function. See \"" + getter->name + "()\" definition at line " + itos(getter->line) + ".", v.line); - return; - } - } +void GDScriptParser::TreePrinter::print_assert(AssertNode *p_assert) { + push_text("Assert ( "); + print_expression(p_assert->condition); + push_line(" )"); +} - if (!found_setter && v.setter != StringName()) { - _set_error("The setter function isn't defined.", v.line); - return; - } +void GDScriptParser::TreePrinter::print_assignment(AssignmentNode *p_assignment) { + switch (p_assignment->assignee->type) { + case Node::IDENTIFIER: + print_identifier(static_cast<IdentifierNode *>(p_assignment->assignee)); + break; + case Node::SUBSCRIPT: + print_subscript(static_cast<SubscriptNode *>(p_assignment->assignee)); + break; + default: + break; // Unreachable. + } - if (!found_getter && v.getter != StringName()) { - _set_error("The getter function isn't defined.", v.line); - return; - } + push_text(" "); + switch (p_assignment->operation) { + case AssignmentNode::OP_ADDITION: + push_text("+"); + break; + case AssignmentNode::OP_SUBTRACTION: + push_text("-"); + break; + case AssignmentNode::OP_MULTIPLICATION: + push_text("*"); + break; + case AssignmentNode::OP_DIVISION: + push_text("/"); + break; + case AssignmentNode::OP_MODULO: + push_text("%"); + break; + case AssignmentNode::OP_BIT_SHIFT_LEFT: + push_text("<<"); + break; + case AssignmentNode::OP_BIT_SHIFT_RIGHT: + push_text(">>"); + break; + case AssignmentNode::OP_BIT_AND: + push_text("&"); + break; + case AssignmentNode::OP_BIT_OR: + push_text("|"); + break; + case AssignmentNode::OP_BIT_XOR: + push_text("^"); + break; + case AssignmentNode::OP_NONE: + break; } + push_text("= "); + print_expression(p_assignment->assigned_value); + push_line(); +} - // Signals - DataType base = p_class->base_type; +void GDScriptParser::TreePrinter::print_await(AwaitNode *p_await) { + push_text("Await "); + print_expression(p_await->to_await); +} - while (base.kind == DataType::CLASS) { - ClassNode *base_class = base.class_type; - for (int i = 0; i < p_class->_signals.size(); i++) { - for (int j = 0; j < base_class->_signals.size(); j++) { - if (p_class->_signals[i].name == base_class->_signals[j].name) { - _set_error("The signal \"" + p_class->_signals[i].name + "\" already exists in a parent class.", p_class->_signals[i].line); - return; - } - } - } - base = base_class->base_type; - } - - StringName native; - if (base.kind == DataType::GDSCRIPT || base.kind == DataType::SCRIPT) { - Ref<Script> scr = base.script_type; - if (scr.is_valid() && scr->is_valid()) { - native = scr->get_instance_base_type(); - for (int i = 0; i < p_class->_signals.size(); i++) { - if (scr->has_script_signal(p_class->_signals[i].name)) { - _set_error("The signal \"" + p_class->_signals[i].name + "\" already exists in a parent class.", p_class->_signals[i].line); - return; - } - } - } - } else if (base.kind == DataType::NATIVE) { - native = base.native_type; +void GDScriptParser::TreePrinter::print_binary_op(BinaryOpNode *p_binary_op) { + // Surround in parenthesis for disambiguation. + push_text("("); + print_expression(p_binary_op->left_operand); + switch (p_binary_op->operation) { + case BinaryOpNode::OP_ADDITION: + push_text(" + "); + break; + case BinaryOpNode::OP_SUBTRACTION: + push_text(" - "); + break; + case BinaryOpNode::OP_MULTIPLICATION: + push_text(" * "); + break; + case BinaryOpNode::OP_DIVISION: + push_text(" / "); + break; + case BinaryOpNode::OP_MODULO: + push_text(" % "); + break; + case BinaryOpNode::OP_BIT_LEFT_SHIFT: + push_text(" << "); + break; + case BinaryOpNode::OP_BIT_RIGHT_SHIFT: + push_text(" >> "); + break; + case BinaryOpNode::OP_BIT_AND: + push_text(" & "); + break; + case BinaryOpNode::OP_BIT_OR: + push_text(" | "); + break; + case BinaryOpNode::OP_BIT_XOR: + push_text(" ^ "); + break; + case BinaryOpNode::OP_LOGIC_AND: + push_text(" AND "); + break; + case BinaryOpNode::OP_LOGIC_OR: + push_text(" OR "); + break; + case BinaryOpNode::OP_TYPE_TEST: + push_text(" IS "); + break; + case BinaryOpNode::OP_CONTENT_TEST: + push_text(" IN "); + break; + case BinaryOpNode::OP_COMP_EQUAL: + push_text(" == "); + break; + case BinaryOpNode::OP_COMP_NOT_EQUAL: + push_text(" != "); + break; + case BinaryOpNode::OP_COMP_LESS: + push_text(" < "); + break; + case BinaryOpNode::OP_COMP_LESS_EQUAL: + push_text(" <= "); + break; + case BinaryOpNode::OP_COMP_GREATER: + push_text(" > "); + break; + case BinaryOpNode::OP_COMP_GREATER_EQUAL: + push_text(" >= "); + break; } + print_expression(p_binary_op->right_operand); + // Surround in parenthesis for disambiguation. + push_text(")"); +} - if (native != StringName()) { - for (int i = 0; i < p_class->_signals.size(); i++) { - if (ClassDB::has_signal(native, p_class->_signals[i].name)) { - _set_error("The signal \"" + p_class->_signals[i].name + "\" already exists in a parent class.", p_class->_signals[i].line); - return; - } +void GDScriptParser::TreePrinter::print_call(CallNode *p_call) { + if (p_call->is_super) { + push_text("super"); + if (p_call->callee != nullptr) { + push_text("."); + print_expression(p_call->callee); } + } else { + print_expression(p_call->callee); } - - // Inner classes - for (int i = 0; i < p_class->subclasses.size(); i++) { - current_class = p_class->subclasses[i]; - _check_class_level_types(current_class); - if (error_set) { - return; + push_text("( "); + for (int i = 0; i < p_call->arguments.size(); i++) { + if (i > 0) { + push_text(" , "); } - current_class = p_class; + print_expression(p_call->arguments[i]); } + push_text(" )"); } -void GDScriptParser::_check_function_types(FunctionNode *p_function) { - p_function->return_type = _resolve_type(p_function->return_type, p_function->line); - - // Arguments - int defaults_ofs = p_function->arguments.size() - p_function->default_values.size(); - for (int i = 0; i < p_function->arguments.size(); i++) { - if (i < defaults_ofs) { - p_function->argument_types.write[i] = _resolve_type(p_function->argument_types[i], p_function->line); - } else { - if (p_function->default_values[i - defaults_ofs]->type != Node::TYPE_OPERATOR) { - _set_error("Parser bug: invalid argument default value.", p_function->line, p_function->column); - return; - } - - OperatorNode *op = static_cast<OperatorNode *>(p_function->default_values[i - defaults_ofs]); - - if (op->op != OperatorNode::OP_ASSIGN || op->arguments.size() != 2) { - _set_error("Parser bug: invalid argument default value operation.", p_function->line); - return; - } +void GDScriptParser::TreePrinter::print_cast(CastNode *p_cast) { + print_expression(p_cast->operand); + push_text(" AS "); + print_type(p_cast->cast_type); +} - DataType def_type = _reduce_node_type(op->arguments[1]); +void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) { + push_text("Class "); + if (p_class->identifier == nullptr) { + push_text("<unnamed>"); + } else { + print_identifier(p_class->identifier); + } - if (p_function->argument_types[i].infer_type) { - def_type.is_constant = false; - p_function->argument_types.write[i] = def_type; + if (p_class->extends_used) { + bool first = true; + push_text(" Extends "); + if (!p_class->extends_path.empty()) { + push_text(vformat(R"("%s")", p_class->extends_path)); + first = false; + } + for (int i = 0; i < p_class->extends.size(); i++) { + if (!first) { + push_text("."); } else { - p_function->argument_types.write[i] = _resolve_type(p_function->argument_types[i], p_function->line); - - if (!_is_type_compatible(p_function->argument_types[i], def_type, true)) { - String arg_name = p_function->arguments[i]; - _set_error("Value type (" + def_type.to_string() + ") doesn't match the type of argument '" + - arg_name + "' (" + p_function->argument_types[i].to_string() + ").", - p_function->line); - } - } - } -#ifdef DEBUG_ENABLED - if (p_function->arguments_usage[i] == 0 && !p_function->arguments[i].operator String().begins_with("_")) { - _add_warning(GDScriptWarning::UNUSED_ARGUMENT, p_function->line, p_function->name, p_function->arguments[i].operator String()); - } - for (int j = 0; j < current_class->variables.size(); j++) { - if (current_class->variables[j].identifier == p_function->arguments[i]) { - _add_warning(GDScriptWarning::SHADOWED_VARIABLE, p_function->line, p_function->arguments[i], itos(current_class->variables[j].line)); + first = false; } + push_text(p_class->extends[i]); } -#endif // DEBUG_ENABLED } - if (!(p_function->name == "_init")) { - // Signature for the initializer may vary -#ifdef DEBUG_ENABLED - DataType return_type; - List<DataType> arg_types; - int default_arg_count = 0; - bool _static = false; - bool vararg = false; - - DataType base_type = current_class->base_type; - if (_get_function_signature(base_type, p_function->name, return_type, arg_types, default_arg_count, _static, vararg)) { - bool valid = _static == p_function->_static; - valid = valid && return_type == p_function->return_type; - int argsize_diff = p_function->arguments.size() - arg_types.size(); - valid = valid && argsize_diff >= 0; - valid = valid && p_function->default_values.size() >= default_arg_count + argsize_diff; - int i = 0; - for (List<DataType>::Element *E = arg_types.front(); valid && E; E = E->next()) { - valid = valid && E->get() == p_function->argument_types[i++]; - } + push_line(" :"); - if (!valid) { - String parent_signature = return_type.has_type ? return_type.to_string() : "Variant"; - if (parent_signature == "null") { - parent_signature = "void"; - } - parent_signature += " " + p_function->name + "("; - if (arg_types.size()) { - int j = 0; - for (List<DataType>::Element *E = arg_types.front(); E; E = E->next()) { - if (E != arg_types.front()) { - parent_signature += ", "; - } - String arg = E->get().to_string(); - if (arg == "null" || arg == "var") { - arg = "Variant"; - } - parent_signature += arg; - if (j == arg_types.size() - default_arg_count) { - parent_signature += "=default"; - } + increase_indent(); - j++; - } - } - parent_signature += ")"; - _set_error("The function signature doesn't match the parent. Parent signature is: \"" + parent_signature + "\".", p_function->line); - return; - } - } -#endif // DEBUG_ENABLED - } else { - if (p_function->return_type.has_type && (p_function->return_type.kind != DataType::BUILTIN || p_function->return_type.builtin_type != Variant::NIL)) { - _set_error("The constructor can't return a value.", p_function->line); - return; - } - } + for (int i = 0; i < p_class->members.size(); i++) { + const ClassNode::Member &m = p_class->members[i]; - if (p_function->return_type.has_type && (p_function->return_type.kind != DataType::BUILTIN || p_function->return_type.builtin_type != Variant::NIL)) { - if (!p_function->body->has_return) { - _set_error("A non-void function must return a value in all possible paths.", p_function->line); - return; + switch (m.type) { + case ClassNode::Member::CLASS: + print_class(m.m_class); + break; + case ClassNode::Member::VARIABLE: + print_variable(m.variable); + break; + case ClassNode::Member::CONSTANT: + print_constant(m.constant); + break; + case ClassNode::Member::SIGNAL: + print_signal(m.signal); + break; + case ClassNode::Member::FUNCTION: + print_function(m.function); + break; + case ClassNode::Member::ENUM: + print_enum(m.m_enum); + break; + case ClassNode::Member::ENUM_VALUE: + break; // Nothing. Will be printed by enum. + case ClassNode::Member::UNDEFINED: + push_line("<unknown member>"); + break; } } - if (p_function->has_yield) { - // yield() will make the function return a GDScriptFunctionState, so the type is ambiguous - p_function->return_type.has_type = false; - p_function->return_type.may_yield = true; - } + decrease_indent(); } -void GDScriptParser::_check_class_blocks_types(ClassNode *p_class) { - // Function blocks - for (int i = 0; i < p_class->static_functions.size(); i++) { - current_function = p_class->static_functions[i]; - current_block = current_function->body; - _mark_line_as_safe(current_function->line); - _check_block_types(current_block); - current_block = nullptr; - current_function = nullptr; - if (error_set) { - return; - } - } +void GDScriptParser::TreePrinter::print_constant(ConstantNode *p_constant) { + push_text("Constant "); + print_identifier(p_constant->identifier); - for (int i = 0; i < p_class->functions.size(); i++) { - current_function = p_class->functions[i]; - current_block = current_function->body; - _mark_line_as_safe(current_function->line); - _check_block_types(current_block); - current_block = nullptr; - current_function = nullptr; - if (error_set) { - return; - } - } + increase_indent(); -#ifdef DEBUG_ENABLED - // Warnings - for (int i = 0; i < p_class->variables.size(); i++) { - if (p_class->variables[i].usages == 0) { - _add_warning(GDScriptWarning::UNUSED_CLASS_VARIABLE, p_class->variables[i].line, p_class->variables[i].identifier); - } - } - for (int i = 0; i < p_class->_signals.size(); i++) { - if (p_class->_signals[i].emissions == 0) { - _add_warning(GDScriptWarning::UNUSED_SIGNAL, p_class->_signals[i].line, p_class->_signals[i].name); - } + push_line(); + push_text("= "); + if (p_constant->initializer == nullptr) { + push_text("<missing value>"); + } else { + print_expression(p_constant->initializer); } -#endif // DEBUG_ENABLED + decrease_indent(); + push_line(); +} - // Inner classes - for (int i = 0; i < p_class->subclasses.size(); i++) { - current_class = p_class->subclasses[i]; - _check_class_blocks_types(current_class); - if (error_set) { - return; +void GDScriptParser::TreePrinter::print_dictionary(DictionaryNode *p_dictionary) { + push_line("{"); + increase_indent(); + for (int i = 0; i < p_dictionary->elements.size(); i++) { + print_expression(p_dictionary->elements[i].key); + if (p_dictionary->style == DictionaryNode::PYTHON_DICT) { + push_text(" : "); + } else { + push_text(" = "); } - current_class = p_class; + print_expression(p_dictionary->elements[i].value); + push_line(" ,"); } + decrease_indent(); + push_text("}"); } -#ifdef DEBUG_ENABLED -static String _find_function_name(const GDScriptParser::OperatorNode *p_call) { - switch (p_call->arguments[0]->type) { - case GDScriptParser::Node::TYPE_TYPE: { - return Variant::get_type_name(static_cast<GDScriptParser::TypeNode *>(p_call->arguments[0])->vtype); - } break; - case GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION: { - return GDScriptFunctions::get_func_name(static_cast<GDScriptParser::BuiltInFunctionNode *>(p_call->arguments[0])->function); - } break; - default: { - int id_index = p_call->op == GDScriptParser::OperatorNode::OP_PARENT_CALL ? 0 : 1; - if (p_call->arguments.size() > id_index && p_call->arguments[id_index]->type == GDScriptParser::Node::TYPE_IDENTIFIER) { - return static_cast<GDScriptParser::IdentifierNode *>(p_call->arguments[id_index])->name; - } - } break; +void GDScriptParser::TreePrinter::print_expression(ExpressionNode *p_expression) { + switch (p_expression->type) { + case Node::ARRAY: + print_array(static_cast<ArrayNode *>(p_expression)); + break; + case Node::ASSIGNMENT: + print_assignment(static_cast<AssignmentNode *>(p_expression)); + break; + case Node::AWAIT: + print_await(static_cast<AwaitNode *>(p_expression)); + break; + case Node::BINARY_OPERATOR: + print_binary_op(static_cast<BinaryOpNode *>(p_expression)); + break; + case Node::CALL: + print_call(static_cast<CallNode *>(p_expression)); + break; + case Node::CAST: + print_cast(static_cast<CastNode *>(p_expression)); + break; + case Node::DICTIONARY: + print_dictionary(static_cast<DictionaryNode *>(p_expression)); + break; + case Node::GET_NODE: + print_get_node(static_cast<GetNodeNode *>(p_expression)); + break; + case Node::IDENTIFIER: + print_identifier(static_cast<IdentifierNode *>(p_expression)); + break; + case Node::LITERAL: + print_literal(static_cast<LiteralNode *>(p_expression)); + break; + case Node::PRELOAD: + print_preload(static_cast<PreloadNode *>(p_expression)); + break; + case Node::SELF: + print_self(static_cast<SelfNode *>(p_expression)); + break; + case Node::SUBSCRIPT: + print_subscript(static_cast<SubscriptNode *>(p_expression)); + break; + case Node::TERNARY_OPERATOR: + print_ternary_op(static_cast<TernaryOpNode *>(p_expression)); + break; + case Node::UNARY_OPERATOR: + print_unary_op(static_cast<UnaryOpNode *>(p_expression)); + break; + default: + push_text(vformat("<unknown expression %d>", p_expression->type)); + break; } - return String(); } -#endif // DEBUG_ENABLED -void GDScriptParser::_check_block_types(BlockNode *p_block) { - Node *last_var_assign = nullptr; - - // Check each statement - for (List<Node *>::Element *E = p_block->statements.front(); E; E = E->next()) { - Node *statement = E->get(); - switch (statement->type) { - case Node::TYPE_NEWLINE: - case Node::TYPE_BREAKPOINT: { - // Nothing to do - } break; - case Node::TYPE_ASSERT: { - AssertNode *an = static_cast<AssertNode *>(statement); - _mark_line_as_safe(an->line); - _reduce_node_type(an->condition); - _reduce_node_type(an->message); - } break; - case Node::TYPE_LOCAL_VAR: { - LocalVarNode *lv = static_cast<LocalVarNode *>(statement); - lv->datatype = _resolve_type(lv->datatype, lv->line); - _mark_line_as_safe(lv->line); - - last_var_assign = lv->assign; - if (lv->assign) { - lv->assign_op->arguments[0]->set_datatype(lv->datatype); - DataType assign_type = _reduce_node_type(lv->assign); -#ifdef DEBUG_ENABLED - if (assign_type.has_type && assign_type.kind == DataType::BUILTIN && assign_type.builtin_type == Variant::NIL) { - if (lv->assign->type == Node::TYPE_OPERATOR) { - OperatorNode *call = static_cast<OperatorNode *>(lv->assign); - if (call->op == OperatorNode::OP_CALL || call->op == OperatorNode::OP_PARENT_CALL) { - _add_warning(GDScriptWarning::VOID_ASSIGNMENT, lv->line, _find_function_name(call)); - } - } - } - if (lv->datatype.has_type && assign_type.may_yield && lv->assign->type == Node::TYPE_OPERATOR) { - _add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, lv->line, _find_function_name(static_cast<OperatorNode *>(lv->assign))); - } - for (int i = 0; i < current_class->variables.size(); i++) { - if (current_class->variables[i].identifier == lv->name) { - _add_warning(GDScriptWarning::SHADOWED_VARIABLE, lv->line, lv->name, itos(current_class->variables[i].line)); - } - } -#endif // DEBUG_ENABLED - - if (!_is_type_compatible(lv->datatype, assign_type)) { - // Try supertype test - if (_is_type_compatible(assign_type, lv->datatype)) { - _mark_line_as_unsafe(lv->line); - } else { - // Try implicit conversion - if (lv->datatype.kind != DataType::BUILTIN || !_is_type_compatible(lv->datatype, assign_type, true)) { - _set_error("The assigned value type (" + assign_type.to_string() + ") doesn't match the variable's type (" + - lv->datatype.to_string() + ").", - lv->line); - return; - } - // Replace assignment with implicit conversion - BuiltInFunctionNode *convert = alloc_node<BuiltInFunctionNode>(); - convert->line = lv->line; - convert->function = GDScriptFunctions::TYPE_CONVERT; - - ConstantNode *tgt_type = alloc_node<ConstantNode>(); - tgt_type->line = lv->line; - tgt_type->value = (int64_t)lv->datatype.builtin_type; - tgt_type->datatype = _type_from_variant(tgt_type->value); - - OperatorNode *convert_call = alloc_node<OperatorNode>(); - convert_call->line = lv->line; - convert_call->op = OperatorNode::OP_CALL; - convert_call->arguments.push_back(convert); - convert_call->arguments.push_back(lv->assign); - convert_call->arguments.push_back(tgt_type); - - lv->assign = convert_call; - lv->assign_op->arguments.write[1] = convert_call; -#ifdef DEBUG_ENABLED - if (lv->datatype.builtin_type == Variant::INT && assign_type.builtin_type == Variant::FLOAT) { - _add_warning(GDScriptWarning::NARROWING_CONVERSION, lv->line); - } -#endif // DEBUG_ENABLED - } - } - if (lv->datatype.infer_type) { - if (!assign_type.has_type) { - _set_error("The assigned value doesn't have a set type; the variable type can't be inferred.", lv->line); - return; - } - if (assign_type.kind == DataType::BUILTIN && assign_type.builtin_type == Variant::NIL) { - _set_error("The variable type cannot be inferred because its value is \"null\".", lv->line); - return; - } - lv->datatype = assign_type; - lv->datatype.is_constant = false; - } - if (lv->datatype.has_type && !assign_type.has_type) { - _mark_line_as_unsafe(lv->line); - } - } - } break; - case Node::TYPE_OPERATOR: { - OperatorNode *op = static_cast<OperatorNode *>(statement); - - switch (op->op) { - case OperatorNode::OP_ASSIGN: - case OperatorNode::OP_ASSIGN_ADD: - case OperatorNode::OP_ASSIGN_SUB: - case OperatorNode::OP_ASSIGN_MUL: - case OperatorNode::OP_ASSIGN_DIV: - case OperatorNode::OP_ASSIGN_MOD: - case OperatorNode::OP_ASSIGN_SHIFT_LEFT: - case OperatorNode::OP_ASSIGN_SHIFT_RIGHT: - case OperatorNode::OP_ASSIGN_BIT_AND: - case OperatorNode::OP_ASSIGN_BIT_OR: - case OperatorNode::OP_ASSIGN_BIT_XOR: { - if (op->arguments.size() < 2) { - _set_error("Parser bug: operation without enough arguments.", op->line, op->column); - return; - } - - if (op->arguments[1] == last_var_assign) { - // Assignment was already checked - break; - } - - _mark_line_as_safe(op->line); - - DataType lh_type = _reduce_node_type(op->arguments[0]); - - if (error_set) { - return; - } - - if (check_types) { - if (!lh_type.has_type) { - if (op->arguments[0]->type == Node::TYPE_OPERATOR) { - _mark_line_as_unsafe(op->line); - } - } - if (lh_type.is_constant) { - _set_error("Can't assign a new value to a constant.", op->line); - return; - } - } - - DataType rh_type; - if (op->op != OperatorNode::OP_ASSIGN) { - // Validate operation - DataType arg_type = _reduce_node_type(op->arguments[1]); - if (!arg_type.has_type) { - _mark_line_as_unsafe(op->line); - break; - } - - Variant::Operator oper = _get_variant_operation(op->op); - bool valid = false; - rh_type = _get_operation_type(oper, lh_type, arg_type, valid); +void GDScriptParser::TreePrinter::print_enum(EnumNode *p_enum) { + push_text("Enum "); + if (p_enum->identifier != nullptr) { + print_identifier(p_enum->identifier); + } else { + push_text("<unnamed>"); + } - if (check_types && !valid) { - _set_error("Invalid operand types (\"" + lh_type.to_string() + "\" and \"" + arg_type.to_string() + - "\") to assignment operator \"" + Variant::get_operator_name(oper) + "\".", - op->line); - return; - } - } else { - rh_type = _reduce_node_type(op->arguments[1]); - } -#ifdef DEBUG_ENABLED - if (rh_type.has_type && rh_type.kind == DataType::BUILTIN && rh_type.builtin_type == Variant::NIL) { - if (op->arguments[1]->type == Node::TYPE_OPERATOR) { - OperatorNode *call = static_cast<OperatorNode *>(op->arguments[1]); - if (call->op == OperatorNode::OP_CALL || call->op == OperatorNode::OP_PARENT_CALL) { - _add_warning(GDScriptWarning::VOID_ASSIGNMENT, op->line, _find_function_name(call)); - } - } - } - if (lh_type.has_type && rh_type.may_yield && op->arguments[1]->type == Node::TYPE_OPERATOR) { - _add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, op->line, _find_function_name(static_cast<OperatorNode *>(op->arguments[1]))); - } + push_line(" {"); + increase_indent(); + for (int i = 0; i < p_enum->values.size(); i++) { + const EnumNode::Value &item = p_enum->values[i]; + print_identifier(item.identifier); + push_text(" = "); + push_text(itos(item.value)); + push_line(" ,"); + } + decrease_indent(); + push_line("}"); +} -#endif // DEBUG_ENABLED - bool type_match = lh_type.has_type && rh_type.has_type; - if (check_types && !_is_type_compatible(lh_type, rh_type)) { - type_match = false; +void GDScriptParser::TreePrinter::print_for(ForNode *p_for) { + push_text("For "); + print_identifier(p_for->variable); + push_text(" IN "); + print_expression(p_for->list); + push_line(" :"); - // Try supertype test - if (_is_type_compatible(rh_type, lh_type)) { - _mark_line_as_unsafe(op->line); - } else { - // Try implicit conversion - if (lh_type.kind != DataType::BUILTIN || !_is_type_compatible(lh_type, rh_type, true)) { - _set_error("The assigned value's type (" + rh_type.to_string() + ") doesn't match the variable's type (" + - lh_type.to_string() + ").", - op->line); - return; - } - if (op->op == OperatorNode::OP_ASSIGN) { - // Replace assignment with implicit conversion - BuiltInFunctionNode *convert = alloc_node<BuiltInFunctionNode>(); - convert->line = op->line; - convert->function = GDScriptFunctions::TYPE_CONVERT; - - ConstantNode *tgt_type = alloc_node<ConstantNode>(); - tgt_type->line = op->line; - tgt_type->value = (int)lh_type.builtin_type; - tgt_type->datatype = _type_from_variant(tgt_type->value); - - OperatorNode *convert_call = alloc_node<OperatorNode>(); - convert_call->line = op->line; - convert_call->op = OperatorNode::OP_CALL; - convert_call->arguments.push_back(convert); - convert_call->arguments.push_back(op->arguments[1]); - convert_call->arguments.push_back(tgt_type); - - op->arguments.write[1] = convert_call; - - type_match = true; // Since we are converting, the type is matching - } -#ifdef DEBUG_ENABLED - if (lh_type.builtin_type == Variant::INT && rh_type.builtin_type == Variant::FLOAT) { - _add_warning(GDScriptWarning::NARROWING_CONVERSION, op->line); - } -#endif // DEBUG_ENABLED - } - } -#ifdef DEBUG_ENABLED - if (!rh_type.has_type && (op->op != OperatorNode::OP_ASSIGN || lh_type.has_type || op->arguments[0]->type == Node::TYPE_OPERATOR)) { - _mark_line_as_unsafe(op->line); - } -#endif // DEBUG_ENABLED - op->datatype.has_type = type_match; - } break; - case OperatorNode::OP_CALL: - case OperatorNode::OP_PARENT_CALL: { - _mark_line_as_safe(op->line); - DataType func_type = _reduce_function_call_type(op); -#ifdef DEBUG_ENABLED - if (func_type.has_type && (func_type.kind != DataType::BUILTIN || func_type.builtin_type != Variant::NIL)) { - // Figure out function name for warning - String func_name = _find_function_name(op); - if (func_name.empty()) { - func_name = "<undetected name>"; - } - _add_warning(GDScriptWarning::RETURN_VALUE_DISCARDED, op->line, func_name); - } -#endif // DEBUG_ENABLED - if (error_set) { - return; - } - } break; - case OperatorNode::OP_YIELD: { - _mark_line_as_safe(op->line); - _reduce_node_type(op); - } break; - default: { - _mark_line_as_safe(op->line); - _reduce_node_type(op); // Test for safety anyway -#ifdef DEBUG_ENABLED - if (op->op == OperatorNode::OP_TERNARY_IF) { - _add_warning(GDScriptWarning::STANDALONE_TERNARY, statement->line); - } else { - _add_warning(GDScriptWarning::STANDALONE_EXPRESSION, statement->line); - } -#endif // DEBUG_ENABLED - } - } - } break; - case Node::TYPE_CONTROL_FLOW: { - ControlFlowNode *cf = static_cast<ControlFlowNode *>(statement); - _mark_line_as_safe(cf->line); - - switch (cf->cf_type) { - case ControlFlowNode::CF_RETURN: { - DataType function_type = current_function->get_datatype(); - - DataType ret_type; - if (cf->arguments.size() > 0) { - ret_type = _reduce_node_type(cf->arguments[0]); - if (error_set) { - return; - } - } + increase_indent(); - if (!function_type.has_type) { - break; - } + print_suite(p_for->loop); - if (function_type.kind == DataType::BUILTIN && function_type.builtin_type == Variant::NIL) { - // Return void, should not have arguments - if (cf->arguments.size() > 0) { - _set_error("A void function cannot return a value.", cf->line, cf->column); - return; - } - } else { - // Return something, cannot be empty - if (cf->arguments.size() == 0) { - _set_error("A non-void function must return a value.", cf->line, cf->column); - return; - } + decrease_indent(); +} - if (!_is_type_compatible(function_type, ret_type)) { - _set_error("The returned value type (" + ret_type.to_string() + ") doesn't match the function return type (" + - function_type.to_string() + ").", - cf->line, cf->column); - return; - } - } - } break; - case ControlFlowNode::CF_MATCH: { - MatchNode *match_node = cf->match; - _transform_match_statment(match_node); - } break; - default: { - if (cf->body_else) { - _mark_line_as_safe(cf->body_else->line); - } - for (int i = 0; i < cf->arguments.size(); i++) { - _reduce_node_type(cf->arguments[i]); - } - } break; - } - } break; - case Node::TYPE_CONSTANT: { - ConstantNode *cn = static_cast<ConstantNode *>(statement); - // Strings are fine since they can be multiline comments - if (cn->value.get_type() == Variant::STRING) { - break; - } - [[fallthrough]]; - } - default: { - _mark_line_as_safe(statement->line); - _reduce_node_type(statement); // Test for safety anyway -#ifdef DEBUG_ENABLED - _add_warning(GDScriptWarning::STANDALONE_EXPRESSION, statement->line); -#endif // DEBUG_ENABLED - } - } +void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function) { + for (const List<AnnotationNode *>::Element *E = p_function->annotations.front(); E != nullptr; E = E->next()) { + print_annotation(E->get()); } - - // Parse sub blocks - for (int i = 0; i < p_block->sub_blocks.size(); i++) { - current_block = p_block->sub_blocks[i]; - _check_block_types(current_block); - current_block = p_block; - if (error_set) { - return; + push_text("Function "); + print_identifier(p_function->identifier); + push_text("( "); + for (int i = 0; i < p_function->parameters.size(); i++) { + if (i > 0) { + push_text(" , "); } + print_parameter(p_function->parameters[i]); } + push_line(" ) :"); + increase_indent(); + print_suite(p_function->body); + decrease_indent(); +} -#ifdef DEBUG_ENABLED - // Warnings check - for (Map<StringName, LocalVarNode *>::Element *E = p_block->variables.front(); E; E = E->next()) { - LocalVarNode *lv = E->get(); - if (!lv->name.operator String().begins_with("_")) { - if (lv->usages == 0) { - _add_warning(GDScriptWarning::UNUSED_VARIABLE, lv->line, lv->name); - } else if (lv->assignments == 0) { - _add_warning(GDScriptWarning::UNASSIGNED_VARIABLE, lv->line, lv->name); +void GDScriptParser::TreePrinter::print_get_node(GetNodeNode *p_get_node) { + push_text("$"); + if (p_get_node->string != nullptr) { + print_literal(p_get_node->string); + } else { + for (int i = 0; i < p_get_node->chain.size(); i++) { + if (i > 0) { + push_text("/"); } + print_identifier(p_get_node->chain[i]); } } -#endif // DEBUG_ENABLED } -void GDScriptParser::_set_error(const String &p_error, int p_line, int p_column) { - if (error_set) { - return; //allow no further errors - } - - error = p_error; - error_line = p_line < 0 ? tokenizer->get_token_line() : p_line; - error_column = p_column < 0 ? tokenizer->get_token_column() : p_column; - error_set = true; +void GDScriptParser::TreePrinter::print_identifier(IdentifierNode *p_identifier) { + push_text(p_identifier->name); } -#ifdef DEBUG_ENABLED -void GDScriptParser::_add_warning(int p_code, int p_line, const String &p_symbol1, const String &p_symbol2, const String &p_symbol3, const String &p_symbol4) { - Vector<String> symbols; - if (!p_symbol1.empty()) { - symbols.push_back(p_symbol1); - } - if (!p_symbol2.empty()) { - symbols.push_back(p_symbol2); - } - if (!p_symbol3.empty()) { - symbols.push_back(p_symbol3); - } - if (!p_symbol4.empty()) { - symbols.push_back(p_symbol4); +void GDScriptParser::TreePrinter::print_if(IfNode *p_if, bool p_is_elif) { + if (p_is_elif) { + push_text("Elif "); + } else { + push_text("If "); } - _add_warning(p_code, p_line, symbols); -} + print_expression(p_if->condition); + push_line(" :"); -void GDScriptParser::_add_warning(int p_code, int p_line, const Vector<String> &p_symbols) { - if (GLOBAL_GET("debug/gdscript/warnings/exclude_addons").booleanize() && base_path.begins_with("res://addons/")) { - return; - } - if (tokenizer->is_ignoring_warnings() || !GLOBAL_GET("debug/gdscript/warnings/enable").booleanize()) { - return; - } - String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower(); - if (tokenizer->get_warning_global_skips().has(warn_name)) { - return; - } - if (!GLOBAL_GET("debug/gdscript/warnings/" + warn_name)) { - return; - } + increase_indent(); + print_suite(p_if->true_block); + decrease_indent(); - GDScriptWarning warn; - warn.code = (GDScriptWarning::Code)p_code; - warn.symbols = p_symbols; - warn.line = p_line == -1 ? tokenizer->get_token_line() : p_line; + // FIXME: Properly detect "elif" blocks. + if (p_if->false_block != nullptr) { + push_line("Else :"); + increase_indent(); + print_suite(p_if->false_block); + decrease_indent(); + } +} - List<GDScriptWarning>::Element *before = nullptr; - for (List<GDScriptWarning>::Element *E = warnings.front(); E; E = E->next()) { - if (E->get().line > warn.line) { +void GDScriptParser::TreePrinter::print_literal(LiteralNode *p_literal) { + // Prefix for string types. + switch (p_literal->value.get_type()) { + case Variant::NODE_PATH: + push_text("^\""); + break; + case Variant::STRING: + push_text("\""); + break; + case Variant::STRING_NAME: + push_text("&\""); + break; + default: break; - } - before = E; } - if (before) { - warnings.insert_after(before, warn); - } else { - warnings.push_front(warn); + push_text(p_literal->value); + // Suffix for string types. + switch (p_literal->value.get_type()) { + case Variant::NODE_PATH: + case Variant::STRING: + case Variant::STRING_NAME: + push_text("\""); + break; + default: + break; } } -#endif // DEBUG_ENABLED - -String GDScriptParser::get_error() const { - return error; -} -int GDScriptParser::get_error_line() const { - return error_line; -} +void GDScriptParser::TreePrinter::print_match(MatchNode *p_match) { + push_text("Match "); + print_expression(p_match->test); + push_line(" :"); -int GDScriptParser::get_error_column() const { - return error_column; -} - -bool GDScriptParser::has_error() const { - return error_set; + increase_indent(); + for (int i = 0; i < p_match->branches.size(); i++) { + print_match_branch(p_match->branches[i]); + } + decrease_indent(); } -Error GDScriptParser::_parse(const String &p_base_path) { - base_path = p_base_path; - - //assume class - ClassNode *main_class = alloc_node<ClassNode>(); - main_class->initializer = alloc_node<BlockNode>(); - main_class->initializer->parent_class = main_class; - main_class->ready = alloc_node<BlockNode>(); - main_class->ready->parent_class = main_class; - current_class = main_class; - - _parse_class(main_class); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_ERROR) { - error_set = false; - _set_error("Parse error: " + tokenizer->get_token_error()); +void GDScriptParser::TreePrinter::print_match_branch(MatchBranchNode *p_match_branch) { + for (int i = 0; i < p_match_branch->patterns.size(); i++) { + if (i > 0) { + push_text(" , "); + } + print_match_pattern(p_match_branch->patterns[i]); } - bool for_completion_error_set = false; - if (error_set && for_completion) { - for_completion_error_set = true; - error_set = false; - } + push_line(" :"); - if (error_set) { - return ERR_PARSE_ERROR; - } + increase_indent(); + print_suite(p_match_branch->block); + decrease_indent(); +} - if (dependencies_only) { - return OK; +void GDScriptParser::TreePrinter::print_match_pattern(PatternNode *p_match_pattern) { + switch (p_match_pattern->pattern_type) { + case PatternNode::PT_LITERAL: + print_literal(p_match_pattern->literal); + break; + case PatternNode::PT_WILDCARD: + push_text("_"); + break; + case PatternNode::PT_REST: + push_text(".."); + break; + case PatternNode::PT_BIND: + push_text("Var "); + print_identifier(p_match_pattern->bind); + break; + case PatternNode::PT_EXPRESSION: + print_expression(p_match_pattern->expression); + break; + case PatternNode::PT_ARRAY: + push_text("[ "); + for (int i = 0; i < p_match_pattern->array.size(); i++) { + if (i > 0) { + push_text(" , "); + } + print_match_pattern(p_match_pattern->array[i]); + } + push_text(" ]"); + break; + case PatternNode::PT_DICTIONARY: + push_text("{ "); + for (int i = 0; i < p_match_pattern->dictionary.size(); i++) { + if (i > 0) { + push_text(" , "); + } + if (p_match_pattern->dictionary[i].key != nullptr) { + // Key can be null for rest pattern. + print_expression(p_match_pattern->dictionary[i].key); + push_text(" : "); + } + print_match_pattern(p_match_pattern->dictionary[i].value_pattern); + } + push_text(" }"); + break; } +} - _determine_inheritance(main_class); - - if (error_set) { - return ERR_PARSE_ERROR; +void GDScriptParser::TreePrinter::print_parameter(ParameterNode *p_parameter) { + print_identifier(p_parameter->identifier); + if (p_parameter->datatype_specifier != nullptr) { + push_text(" : "); + print_type(p_parameter->datatype_specifier); } - - current_class = main_class; - current_function = nullptr; - current_block = nullptr; - - if (for_completion) { - check_types = false; + if (p_parameter->default_value != nullptr) { + push_text(" = "); + print_expression(p_parameter->default_value); } +} - // Resolve all class-level stuff before getting into function blocks - _check_class_level_types(main_class); +void GDScriptParser::TreePrinter::print_preload(PreloadNode *p_preload) { + push_text(R"(Preload ( ")"); + push_text(p_preload->resolved_path); + push_text(R"(" )"); +} - if (error_set) { - return ERR_PARSE_ERROR; +void GDScriptParser::TreePrinter::print_return(ReturnNode *p_return) { + push_text("Return"); + if (p_return->return_value != nullptr) { + push_text(" "); + print_expression(p_return->return_value); } + push_line(); +} - // Resolve the function blocks - _check_class_blocks_types(main_class); - - if (for_completion_error_set) { - error_set = true; +void GDScriptParser::TreePrinter::print_self(SelfNode *p_self) { + push_text("Self("); + if (p_self->current_class->identifier != nullptr) { + print_identifier(p_self->current_class->identifier); + } else { + push_text("<main class>"); } + push_text(")"); +} - if (error_set) { - return ERR_PARSE_ERROR; +void GDScriptParser::TreePrinter::print_signal(SignalNode *p_signal) { + push_text("Signal "); + print_identifier(p_signal->identifier); + push_text("( "); + for (int i = 0; i < p_signal->parameters.size(); i++) { + print_parameter(p_signal->parameters[i]); } + push_line(" )"); +} -#ifdef DEBUG_ENABLED - - // Resolve warning ignores - Vector<Pair<int, String>> warning_skips = tokenizer->get_warning_skips(); - bool warning_is_error = GLOBAL_GET("debug/gdscript/warnings/treat_warnings_as_errors").booleanize(); - for (List<GDScriptWarning>::Element *E = warnings.front(); E;) { - GDScriptWarning &w = E->get(); - int skip_index = -1; - for (int i = 0; i < warning_skips.size(); i++) { - if (warning_skips[i].first >= w.line) { - break; - } - skip_index = i; - } - List<GDScriptWarning>::Element *next = E->next(); - bool erase = false; - if (skip_index != -1) { - if (warning_skips[skip_index].second == GDScriptWarning::get_name_from_code(w.code).to_lower()) { - erase = true; - } - warning_skips.remove(skip_index); - } - if (erase) { - warnings.erase(E); - } else if (warning_is_error) { - _set_error(w.get_message() + " (warning treated as error)", w.line); - return ERR_PARSE_ERROR; - } - E = next; +void GDScriptParser::TreePrinter::print_subscript(SubscriptNode *p_subscript) { + print_expression(p_subscript->base); + if (p_subscript->is_attribute) { + push_text("."); + print_identifier(p_subscript->attribute); + } else { + push_text("[ "); + print_expression(p_subscript->index); + push_text(" ]"); } -#endif // DEBUG_ENABLED - - return OK; } -Error GDScriptParser::parse_bytecode(const Vector<uint8_t> &p_bytecode, const String &p_base_path, const String &p_self_path) { - clear(); - - self_path = p_self_path; - GDScriptTokenizerBuffer *tb = memnew(GDScriptTokenizerBuffer); - tb->set_code_buffer(p_bytecode); - tokenizer = tb; - Error ret = _parse(p_base_path); - memdelete(tb); - tokenizer = nullptr; - return ret; +void GDScriptParser::TreePrinter::print_statement(Node *p_statement) { + switch (p_statement->type) { + case Node::ASSERT: + print_assert(static_cast<AssertNode *>(p_statement)); + break; + case Node::VARIABLE: + print_variable(static_cast<VariableNode *>(p_statement)); + break; + case Node::CONSTANT: + print_constant(static_cast<ConstantNode *>(p_statement)); + break; + case Node::IF: + print_if(static_cast<IfNode *>(p_statement)); + break; + case Node::FOR: + print_for(static_cast<ForNode *>(p_statement)); + break; + case Node::WHILE: + print_while(static_cast<WhileNode *>(p_statement)); + break; + case Node::MATCH: + print_match(static_cast<MatchNode *>(p_statement)); + break; + case Node::RETURN: + print_return(static_cast<ReturnNode *>(p_statement)); + break; + case Node::BREAK: + push_line("Break"); + break; + case Node::CONTINUE: + push_line("Continue"); + break; + case Node::PASS: + push_line("Pass"); + break; + case Node::BREAKPOINT: + push_line("Breakpoint"); + break; + case Node::ASSIGNMENT: + print_assignment(static_cast<AssignmentNode *>(p_statement)); + break; + default: + if (p_statement->is_expression()) { + print_expression(static_cast<ExpressionNode *>(p_statement)); + push_line(); + } else { + push_line(vformat("<unknown statement %d>", p_statement->type)); + } + break; + } } -Error GDScriptParser::parse(const String &p_code, const String &p_base_path, bool p_just_validate, const String &p_self_path, bool p_for_completion, Set<int> *r_safe_lines, bool p_dependencies_only) { - clear(); - - self_path = p_self_path; - GDScriptTokenizerText *tt = memnew(GDScriptTokenizerText); - tt->set_code(p_code); - - validating = p_just_validate; - for_completion = p_for_completion; - dependencies_only = p_dependencies_only; -#ifdef DEBUG_ENABLED - safe_lines = r_safe_lines; -#endif // DEBUG_ENABLED - tokenizer = tt; - Error ret = _parse(p_base_path); - memdelete(tt); - tokenizer = nullptr; - return ret; +void GDScriptParser::TreePrinter::print_suite(SuiteNode *p_suite) { + for (int i = 0; i < p_suite->statements.size(); i++) { + print_statement(p_suite->statements[i]); + } } -bool GDScriptParser::is_tool_script() const { - return (head && head->type == Node::TYPE_CLASS && static_cast<const ClassNode *>(head)->tool); +void GDScriptParser::TreePrinter::print_ternary_op(TernaryOpNode *p_ternary_op) { + // Surround in parenthesis for disambiguation. + push_text("("); + print_expression(p_ternary_op->true_expr); + push_text(") IF ("); + print_expression(p_ternary_op->condition); + push_text(") ELSE ("); + print_expression(p_ternary_op->false_expr); + push_text(")"); } -const GDScriptParser::Node *GDScriptParser::get_parse_tree() const { - return head; +void GDScriptParser::TreePrinter::print_type(TypeNode *p_type) { + if (p_type->type_chain.empty()) { + push_text("Void"); + } else { + for (int i = 0; i < p_type->type_chain.size(); i++) { + if (i > 0) { + push_text("."); + } + print_identifier(p_type->type_chain[i]); + } + } } -void GDScriptParser::clear() { - while (list) { - Node *l = list; - list = list->next; - memdelete(l); +void GDScriptParser::TreePrinter::print_unary_op(UnaryOpNode *p_unary_op) { + // Surround in parenthesis for disambiguation. + push_text("("); + switch (p_unary_op->operation) { + case UnaryOpNode::OP_POSITIVE: + push_text("+"); + break; + case UnaryOpNode::OP_NEGATIVE: + push_text("-"); + break; + case UnaryOpNode::OP_LOGIC_NOT: + push_text("NOT"); + break; + case UnaryOpNode::OP_COMPLEMENT: + push_text("~"); + break; } - - head = nullptr; - list = nullptr; - - completion_type = COMPLETION_NONE; - completion_node = nullptr; - completion_class = nullptr; - completion_function = nullptr; - completion_block = nullptr; - current_block = nullptr; - current_class = nullptr; - - completion_found = false; - rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; - - current_function = nullptr; - - validating = false; - for_completion = false; - error_set = false; - indent_level.clear(); - indent_level.push_back(IndentLevel(0, 0)); - error_line = 0; - error_column = 0; - pending_newline = -1; - parenthesis = 0; - current_export.type = Variant::NIL; - check_types = true; - dependencies_only = false; - dependencies.clear(); - error = ""; -#ifdef DEBUG_ENABLED - safe_lines = nullptr; -#endif // DEBUG_ENABLED + print_expression(p_unary_op->operand); + // Surround in parenthesis for disambiguation. + push_text(")"); } -GDScriptParser::CompletionType GDScriptParser::get_completion_type() { - return completion_type; -} +void GDScriptParser::TreePrinter::print_variable(VariableNode *p_variable) { + for (const List<AnnotationNode *>::Element *E = p_variable->annotations.front(); E != nullptr; E = E->next()) { + print_annotation(E->get()); + } -StringName GDScriptParser::get_completion_cursor() { - return completion_cursor; -} + push_text("Variable "); + print_identifier(p_variable->identifier); -int GDScriptParser::get_completion_line() { - return completion_line; -} + push_text(" : "); + if (p_variable->datatype_specifier != nullptr) { + print_type(p_variable->datatype_specifier); + } else if (p_variable->infer_datatype) { + push_text("<inferred type>"); + } else { + push_text("Variant"); + } -Variant::Type GDScriptParser::get_completion_built_in_constant() { - return completion_built_in_constant; -} + increase_indent(); -GDScriptParser::Node *GDScriptParser::get_completion_node() { - return completion_node; -} + push_line(); + push_text("= "); + if (p_variable->initializer == nullptr) { + push_text("<default value>"); + } else { + print_expression(p_variable->initializer); + } + push_line(); + + if (p_variable->property != VariableNode::PROP_NONE) { + if (p_variable->getter != nullptr) { + push_text("Get"); + if (p_variable->property == VariableNode::PROP_INLINE) { + push_line(":"); + increase_indent(); + print_suite(p_variable->getter); + decrease_indent(); + } else { + push_line(" ="); + increase_indent(); + print_identifier(p_variable->getter_pointer); + push_line(); + decrease_indent(); + } + } + if (p_variable->setter != nullptr) { + push_text("Set ("); + if (p_variable->property == VariableNode::PROP_INLINE) { + if (p_variable->setter_parameter != nullptr) { + print_identifier(p_variable->setter_parameter); + } else { + push_text("<missing>"); + } + push_line("):"); + increase_indent(); + print_suite(p_variable->setter); + decrease_indent(); + } else { + push_line(" ="); + increase_indent(); + print_identifier(p_variable->setter_pointer); + push_line(); + decrease_indent(); + } + } + } -GDScriptParser::BlockNode *GDScriptParser::get_completion_block() { - return completion_block; + decrease_indent(); + push_line(); } -GDScriptParser::ClassNode *GDScriptParser::get_completion_class() { - return completion_class; -} +void GDScriptParser::TreePrinter::print_while(WhileNode *p_while) { + push_text("While "); + print_expression(p_while->condition); + push_line(" :"); -GDScriptParser::FunctionNode *GDScriptParser::get_completion_function() { - return completion_function; + increase_indent(); + print_suite(p_while->loop); + decrease_indent(); } -int GDScriptParser::get_completion_argument_index() { - return completion_argument; -} +void GDScriptParser::TreePrinter::print_tree(const GDScriptParser &p_parser) { + ERR_FAIL_COND_MSG(p_parser.get_tree() == nullptr, "Parse the code before printing the parse tree."); -bool GDScriptParser::get_completion_identifier_is_function() { - return completion_ident_is_call; -} + if (p_parser.is_tool()) { + push_line("@tool"); + } + if (!p_parser.get_tree()->icon_path.empty()) { + push_text(R"(@icon (")"); + push_text(p_parser.get_tree()->icon_path); + push_line("\")"); + } + print_class(p_parser.get_tree()); -GDScriptParser::GDScriptParser() { - head = nullptr; - list = nullptr; - tokenizer = nullptr; - pending_newline = -1; - clear(); + print_line(printed); } -GDScriptParser::~GDScriptParser() { - clear(); -} +#endif // DEBUG_ENABLED diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 7dedb6d6f9..7d8ae7fc55 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -31,665 +31,1322 @@ #ifndef GDSCRIPT_PARSER_H #define GDSCRIPT_PARSER_H +#include "core/hash_map.h" +#include "core/io/multiplayer_api.h" +#include "core/list.h" #include "core/map.h" -#include "core/object.h" +#include "core/reference.h" +#include "core/resource.h" #include "core/script_language.h" +#include "core/string_name.h" +#include "core/ustring.h" +#include "core/variant.h" +#include "core/vector.h" +#include "gdscript_cache.h" #include "gdscript_functions.h" #include "gdscript_tokenizer.h" -struct GDScriptDataType; -struct GDScriptWarning; +#ifdef DEBUG_ENABLED +#include "core/string_builder.h" +#include "gdscript_warning.h" +#endif // DEBUG_ENABLED class GDScriptParser { + struct AnnotationInfo; + public: + // Forward-declare all parser nodes, to avoid ordering issues. + struct AnnotationNode; + struct ArrayNode; + struct AssertNode; + struct AssignmentNode; + struct AwaitNode; + struct BinaryOpNode; + struct BreakNode; + struct BreakpointNode; + struct CallNode; + struct CastNode; struct ClassNode; + struct ConstantNode; + struct ContinueNode; + struct DictionaryNode; + struct EnumNode; + struct ExpressionNode; + struct ForNode; + struct FunctionNode; + struct GetNodeNode; + struct IdentifierNode; + struct IfNode; + struct LiteralNode; + struct MatchNode; + struct MatchBranchNode; + struct ParameterNode; + struct PassNode; + struct PatternNode; + struct PreloadNode; + struct ReturnNode; + struct SelfNode; + struct SignalNode; + struct SubscriptNode; + struct SuiteNode; + struct TernaryOpNode; + struct TypeNode; + struct UnaryOpNode; + struct VariableNode; + struct WhileNode; struct DataType { enum Kind { BUILTIN, NATIVE, SCRIPT, - GDSCRIPT, - CLASS, - UNRESOLVED + CLASS, // GDScript. + ENUM, // Full enumeration. + ENUM_VALUE, // Value from enumeration. + VARIANT, // Can be any type. + UNRESOLVED, + // TODO: Enum }; - Kind kind = UNRESOLVED; - bool has_type = false; + enum TypeSource { + UNDETECTED, // Can be any type. + INFERRED, // Has inferred type, but still dynamic. + ANNOTATED_EXPLICIT, // Has a specific type annotated. + ANNOTATED_INFERRED, // Has a static type but comes from the assigned value. + }; + TypeSource type_source = UNDETECTED; + bool is_constant = false; - bool is_meta_type = false; // Whether the value can be used as a type - bool infer_type = false; - bool may_yield = false; // For function calls + bool is_meta_type = false; + bool is_coroutine = false; // For function calls. Variant::Type builtin_type = Variant::NIL; StringName native_type; + StringName enum_type; // Enum name or the value name in an enum. Ref<Script> script_type; + String script_path; ClassNode *class_type = nullptr; + MethodInfo method_info; // For callable/signals. + HashMap<StringName, int> enum_values; // For enums. + + _FORCE_INLINE_ bool is_set() const { return kind != UNRESOLVED; } + _FORCE_INLINE_ bool has_no_type() const { return type_source == UNDETECTED; } + _FORCE_INLINE_ bool is_variant() const { return kind == VARIANT || kind == UNRESOLVED; } + _FORCE_INLINE_ bool is_hard_type() const { return type_source > INFERRED; } String to_string() const; - bool operator==(const DataType &other) const { - if (!has_type || !other.has_type) { - return true; // Can be considered equal for parsing purpose + bool operator==(const DataType &p_other) const { + if (type_source == UNDETECTED || p_other.type_source == UNDETECTED) { + return true; // Can be consireded equal for parsing purposes. } - if (kind != other.kind) { + + if (type_source == INFERRED || p_other.type_source == INFERRED) { + return true; // Can be consireded equal for parsing purposes. + } + + if (kind != p_other.kind) { return false; } + switch (kind) { - case BUILTIN: { - return builtin_type == other.builtin_type; - } break; - case NATIVE: { - return native_type == other.native_type; - } break; - case GDSCRIPT: - case SCRIPT: { - return script_type == other.script_type; - } break; - case CLASS: { - return class_type == other.class_type; - } break; - case UNRESOLVED: { - } break; + case VARIANT: + return true; // All variants are the same. + case BUILTIN: + return builtin_type == p_other.builtin_type; + case NATIVE: + case ENUM: + return native_type == p_other.native_type; + case ENUM_VALUE: + return native_type == p_other.native_type && enum_type == p_other.enum_type; + case SCRIPT: + return script_type == p_other.script_type; + case CLASS: + return class_type == p_other.class_type; + case UNRESOLVED: + break; } + return false; } - DataType() {} + bool operator!=(const DataType &p_other) const { + return !(this->operator==(p_other)); + } + }; + + struct ParserError { + // TODO: Do I really need a "type"? + // enum Type { + // NO_ERROR, + // EMPTY_FILE, + // CLASS_NAME_USED_TWICE, + // EXTENDS_USED_TWICE, + // EXPECTED_END_STATEMENT, + // }; + // Type type = NO_ERROR; + String message; + int line = 0, column = 0; }; struct Node { enum Type { - TYPE_CLASS, - TYPE_FUNCTION, - TYPE_BUILT_IN_FUNCTION, - TYPE_BLOCK, - TYPE_IDENTIFIER, - TYPE_TYPE, - TYPE_CONSTANT, - TYPE_ARRAY, - TYPE_DICTIONARY, - TYPE_SELF, - TYPE_OPERATOR, - TYPE_CONTROL_FLOW, - TYPE_LOCAL_VAR, - TYPE_CAST, - TYPE_ASSERT, - TYPE_BREAKPOINT, - TYPE_NEWLINE, + NONE, + ANNOTATION, + ARRAY, + ASSERT, + ASSIGNMENT, + AWAIT, + BINARY_OPERATOR, + BREAK, + BREAKPOINT, + CALL, + CAST, + CLASS, + CONSTANT, + CONTINUE, + DICTIONARY, + ENUM, + FOR, + FUNCTION, + GET_NODE, + IDENTIFIER, + IF, + LITERAL, + MATCH, + MATCH_BRANCH, + PARAMETER, + PASS, + PATTERN, + PRELOAD, + RETURN, + SELF, + SIGNAL, + SUBSCRIPT, + SUITE, + TERNARY_OPERATOR, + TYPE, + UNARY_OPERATOR, + VARIABLE, + WHILE, }; - Node *next; - int line; - int column; - Type type; + Type type = NONE; + int start_line = 0, end_line = 0; + int start_column = 0, end_column = 0; + int leftmost_column = 0, rightmost_column = 0; + Node *next = nullptr; + List<AnnotationNode *> annotations; - virtual DataType get_datatype() const { return DataType(); } - virtual void set_datatype(const DataType &p_datatype) {} + DataType datatype; + + virtual DataType get_datatype() const { return datatype; } + virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } + + virtual bool is_expression() const { return false; } virtual ~Node() {} }; - struct FunctionNode; - struct BlockNode; - struct ConstantNode; - struct LocalVarNode; - struct OperatorNode; + struct ExpressionNode : public Node { + // Base type for all expression kinds. + bool reduced = false; + bool is_constant = false; + Variant reduced_value; - struct ClassNode : public Node { - bool tool; + virtual bool is_expression() const { return true; } + virtual ~ExpressionNode() {} + + protected: + ExpressionNode() {} + }; + + struct AnnotationNode : public Node { StringName name; - bool extends_used; - bool classname_used; - StringName extends_file; - Vector<StringName> extends_class; - DataType base_type; - String icon_path; + Vector<ExpressionNode *> arguments; + Vector<Variant> resolved_arguments; - struct Member { - PropertyInfo _export; -#ifdef TOOLS_ENABLED - Variant default_value; -#endif - StringName identifier; - DataType data_type; - StringName setter; - StringName getter; - int line; - Node *expression; - OperatorNode *initial_assignment; - MultiplayerAPI::RPCMode rpc_mode; - int usages; - }; + AnnotationInfo *info = nullptr; - struct Constant { - Node *expression; - DataType type; - }; + bool apply(GDScriptParser *p_this, Node *p_target) const; + bool applies_to(uint32_t p_target_kinds) const; - struct Signal { - StringName name; - Vector<StringName> arguments; - int emissions; - int line; - }; + AnnotationNode() { + type = ANNOTATION; + } + }; - Vector<ClassNode *> subclasses; - Vector<Member> variables; - Map<StringName, Constant> constant_expressions; - Vector<FunctionNode *> functions; - Vector<FunctionNode *> static_functions; - Vector<Signal> _signals; - BlockNode *initializer; - BlockNode *ready; - ClassNode *owner; - //Vector<Node*> initializers; - int end_line; + struct ArrayNode : public ExpressionNode { + Vector<ExpressionNode *> elements; - ClassNode() { - tool = false; - type = TYPE_CLASS; - extends_used = false; - classname_used = false; - end_line = -1; - owner = nullptr; + ArrayNode() { + type = ARRAY; } }; - struct FunctionNode : public Node { - bool _static; - MultiplayerAPI::RPCMode rpc_mode; - bool has_yield; - bool has_unreachable_code; - StringName name; - DataType return_type; - Vector<StringName> arguments; - Vector<DataType> argument_types; - Vector<Node *> default_values; - BlockNode *body; -#ifdef DEBUG_ENABLED - Vector<int> arguments_usage; -#endif // DEBUG_ENABLED + struct AssertNode : public Node { + ExpressionNode *condition = nullptr; + LiteralNode *message = nullptr; - virtual DataType get_datatype() const { return return_type; } - virtual void set_datatype(const DataType &p_datatype) { return_type = p_datatype; } - int get_required_argument_count() { return arguments.size() - default_values.size(); } + AssertNode() { + type = ASSERT; + } + }; - FunctionNode() { - type = TYPE_FUNCTION; - _static = false; - rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; - has_yield = false; - has_unreachable_code = false; + struct AssignmentNode : public ExpressionNode { + // Assignment is not really an expression but it's easier to parse as if it were. + enum Operation { + OP_NONE, + OP_ADDITION, + OP_SUBTRACTION, + OP_MULTIPLICATION, + OP_DIVISION, + OP_MODULO, + OP_BIT_SHIFT_LEFT, + OP_BIT_SHIFT_RIGHT, + OP_BIT_AND, + OP_BIT_OR, + OP_BIT_XOR, + }; + + Operation operation = OP_NONE; + Variant::Operator variant_op = Variant::OP_MAX; + ExpressionNode *assignee = nullptr; + ExpressionNode *assigned_value = nullptr; + + AssignmentNode() { + type = ASSIGNMENT; } }; - struct BlockNode : public Node { - ClassNode *parent_class = nullptr; - BlockNode *parent_block = nullptr; - List<Node *> statements; - Map<StringName, LocalVarNode *> variables; - bool has_return = false; - bool can_break = false; - bool can_continue = false; + struct AwaitNode : public ExpressionNode { + ExpressionNode *to_await = nullptr; - Node *if_condition = nullptr; //tiny hack to improve code completion on if () blocks + AwaitNode() { + type = AWAIT; + } + }; + + struct BinaryOpNode : public ExpressionNode { + enum OpType { + OP_ADDITION, + OP_SUBTRACTION, + OP_MULTIPLICATION, + OP_DIVISION, + OP_MODULO, + OP_BIT_LEFT_SHIFT, + OP_BIT_RIGHT_SHIFT, + OP_BIT_AND, + OP_BIT_OR, + OP_BIT_XOR, + OP_LOGIC_AND, + OP_LOGIC_OR, + OP_TYPE_TEST, + OP_CONTENT_TEST, + OP_COMP_EQUAL, + OP_COMP_NOT_EQUAL, + OP_COMP_LESS, + OP_COMP_LESS_EQUAL, + OP_COMP_GREATER, + OP_COMP_GREATER_EQUAL, + }; - //the following is useful for code completion - List<BlockNode *> sub_blocks; - int end_line = -1; + OpType operation; + Variant::Operator variant_op = Variant::OP_MAX; + ExpressionNode *left_operand = nullptr; + ExpressionNode *right_operand = nullptr; - BlockNode() { - type = TYPE_BLOCK; + BinaryOpNode() { + type = BINARY_OPERATOR; } }; - struct TypeNode : public Node { - Variant::Type vtype; + struct BreakNode : public Node { + BreakNode() { + type = BREAK; + } + }; - TypeNode() { - type = TYPE_TYPE; + struct BreakpointNode : public Node { + BreakpointNode() { + type = BREAKPOINT; + } + }; + + struct CallNode : public ExpressionNode { + ExpressionNode *callee = nullptr; + Vector<ExpressionNode *> arguments; + StringName function_name; + bool is_super = false; + + CallNode() { + type = CALL; + } + + Type get_callee_type() const { + if (callee == nullptr) { + return Type::NONE; + } else { + return callee->type; + } } }; - struct BuiltInFunctionNode : public Node { - GDScriptFunctions::Function function; + struct CastNode : public ExpressionNode { + ExpressionNode *operand = nullptr; + TypeNode *cast_type = nullptr; - BuiltInFunctionNode() { - type = TYPE_BUILT_IN_FUNCTION; + CastNode() { + type = CAST; } }; - struct IdentifierNode : public Node { - StringName name; - BlockNode *declared_block = nullptr; // Simplify lookup by checking if it is declared locally - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } + struct EnumNode : public Node { + struct Value { + IdentifierNode *identifier = nullptr; + ExpressionNode *custom_value = nullptr; + EnumNode *parent_enum = nullptr; + int index = -1; + bool resolved = false; + int value = 0; + int line = 0; + int leftmost_column = 0; + int rightmost_column = 0; + }; + IdentifierNode *identifier = nullptr; + Vector<Value> values; - IdentifierNode() { - type = TYPE_IDENTIFIER; + EnumNode() { + type = ENUM; } }; - struct LocalVarNode : public Node { - StringName name; - Node *assign = nullptr; - OperatorNode *assign_op = nullptr; - int assignments = 0; - int usages = 0; - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } + struct ClassNode : public Node { + struct Member { + enum Type { + UNDEFINED, + CLASS, + CONSTANT, + FUNCTION, + SIGNAL, + VARIABLE, + ENUM, + ENUM_VALUE, // For unnamed enums. + }; + + Type type = UNDEFINED; + + union { + ClassNode *m_class = nullptr; + ConstantNode *constant; + FunctionNode *function; + SignalNode *signal; + VariableNode *variable; + EnumNode *m_enum; + }; + EnumNode::Value enum_value; + + String get_type_name() const { + switch (type) { + case UNDEFINED: + return "???"; + case CLASS: + return "class"; + case CONSTANT: + return "constant"; + case FUNCTION: + return "function"; + case SIGNAL: + return "signal"; + case VARIABLE: + return "variable"; + case ENUM: + return "enum"; + case ENUM_VALUE: + return "enum value"; + } + return ""; + } + + int get_line() const { + switch (type) { + case CLASS: + return m_class->start_line; + case CONSTANT: + return constant->start_line; + case FUNCTION: + return function->start_line; + case VARIABLE: + return variable->start_line; + case ENUM_VALUE: + return enum_value.line; + case ENUM: + return m_enum->start_line; + case SIGNAL: + return signal->start_line; + case UNDEFINED: + ERR_FAIL_V_MSG(-1, "Reaching undefined member type."); + } + ERR_FAIL_V_MSG(-1, "Reaching unhandled type."); + } + + DataType get_datatype() const { + switch (type) { + case CLASS: + return m_class->get_datatype(); + case CONSTANT: + return constant->get_datatype(); + case FUNCTION: + return function->get_datatype(); + case VARIABLE: + return variable->get_datatype(); + case ENUM: + return m_enum->get_datatype(); + case ENUM_VALUE: { + // Always integer. + DataType type; + type.type_source = DataType::ANNOTATED_EXPLICIT; + type.kind = DataType::BUILTIN; + type.builtin_type = Variant::INT; + return type; + } + case SIGNAL: { + DataType type; + type.type_source = DataType::ANNOTATED_EXPLICIT; + type.kind = DataType::BUILTIN; + type.builtin_type = Variant::SIGNAL; + // TODO: Add parameter info. + return type; + } + case UNDEFINED: + return DataType(); + } + ERR_FAIL_V_MSG(DataType(), "Reaching unhandled type."); + } + + Member() {} + + Member(ClassNode *p_class) { + type = CLASS; + m_class = p_class; + } + Member(ConstantNode *p_constant) { + type = CONSTANT; + constant = p_constant; + } + Member(VariableNode *p_variable) { + type = VARIABLE; + variable = p_variable; + } + Member(SignalNode *p_signal) { + type = SIGNAL; + signal = p_signal; + } + Member(FunctionNode *p_function) { + type = FUNCTION; + function = p_function; + } + Member(EnumNode *p_enum) { + type = ENUM; + m_enum = p_enum; + } + Member(const EnumNode::Value &p_enum_value) { + type = ENUM_VALUE; + enum_value = p_enum_value; + } + }; - LocalVarNode() { - type = TYPE_LOCAL_VAR; + IdentifierNode *identifier = nullptr; + String icon_path; + Vector<Member> members; + HashMap<StringName, int> members_indices; + ClassNode *outer = nullptr; + bool extends_used = false; + bool onready_used = false; + String extends_path; + Vector<StringName> extends; // List for indexing: extends A.B.C + DataType base_type; + String fqcn; // Fully-qualified class name. Identifies uniquely any class in the project. + + bool resolved_interface = false; + bool resolved_body = false; + + Member get_member(const StringName &p_name) const { + return members[members_indices[p_name]]; + } + bool has_member(const StringName &p_name) const { + return members_indices.has(p_name); + } + bool has_function(const StringName &p_name) const { + return has_member(p_name) && members[members_indices[p_name]].type == Member::FUNCTION; + } + template <class T> + void add_member(T *p_member_node) { + members_indices[p_member_node->identifier->name] = members.size(); + members.push_back(Member(p_member_node)); + } + void add_member(const EnumNode::Value &p_enum_value) { + members_indices[p_enum_value.identifier->name] = members.size(); + members.push_back(Member(p_enum_value)); + } + + ClassNode() { + type = CLASS; } }; struct ConstantNode : public Node { - Variant value; - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } + IdentifierNode *identifier = nullptr; + ExpressionNode *initializer = nullptr; + TypeNode *datatype_specifier = nullptr; + bool infer_datatype = false; + int usages = 0; ConstantNode() { - type = TYPE_CONSTANT; + type = CONSTANT; } }; - struct ArrayNode : public Node { - Vector<Node *> elements; - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } - - ArrayNode() { - type = TYPE_ARRAY; - datatype.has_type = true; - datatype.kind = DataType::BUILTIN; - datatype.builtin_type = Variant::ARRAY; + struct ContinueNode : public Node { + ContinueNode() { + type = CONTINUE; } }; - struct DictionaryNode : public Node { + struct DictionaryNode : public ExpressionNode { struct Pair { - Node *key; - Node *value; + ExpressionNode *key = nullptr; + ExpressionNode *value = nullptr; }; - Vector<Pair> elements; - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } + + enum Style { + LUA_TABLE, + PYTHON_DICT, + }; + Style style = PYTHON_DICT; DictionaryNode() { - type = TYPE_DICTIONARY; - datatype.has_type = true; - datatype.kind = DataType::BUILTIN; - datatype.builtin_type = Variant::DICTIONARY; + type = DICTIONARY; } }; - struct SelfNode : public Node { - SelfNode() { - type = TYPE_SELF; - } - }; - - struct OperatorNode : public Node { - enum Operator { - //call/constructor operator - OP_CALL, - OP_PARENT_CALL, - OP_YIELD, - OP_IS, - OP_IS_BUILTIN, - //indexing operator - OP_INDEX, - OP_INDEX_NAMED, - //unary operators - OP_NEG, - OP_POS, - OP_NOT, - OP_BIT_INVERT, - //binary operators (in precedence order) - OP_IN, - OP_EQUAL, - OP_NOT_EQUAL, - OP_LESS, - OP_LESS_EQUAL, - OP_GREATER, - OP_GREATER_EQUAL, - OP_AND, - OP_OR, - OP_ADD, - OP_SUB, - OP_MUL, - OP_DIV, - OP_MOD, - OP_SHIFT_LEFT, - OP_SHIFT_RIGHT, - OP_INIT_ASSIGN, - OP_ASSIGN, - OP_ASSIGN_ADD, - OP_ASSIGN_SUB, - OP_ASSIGN_MUL, - OP_ASSIGN_DIV, - OP_ASSIGN_MOD, - OP_ASSIGN_SHIFT_LEFT, - OP_ASSIGN_SHIFT_RIGHT, - OP_ASSIGN_BIT_AND, - OP_ASSIGN_BIT_OR, - OP_ASSIGN_BIT_XOR, - OP_BIT_AND, - OP_BIT_OR, - OP_BIT_XOR, - //ternary operators - OP_TERNARY_IF, - OP_TERNARY_ELSE, - }; + struct ForNode : public Node { + IdentifierNode *variable = nullptr; + ExpressionNode *list = nullptr; + SuiteNode *loop = nullptr; - Operator op; + ForNode() { + type = FOR; + } + }; - Vector<Node *> arguments; - DataType datatype; - virtual DataType get_datatype() const { return datatype; } - virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; } - OperatorNode() { - type = TYPE_OPERATOR; + struct FunctionNode : public Node { + IdentifierNode *identifier = nullptr; + Vector<ParameterNode *> parameters; + HashMap<StringName, int> parameters_indices; + TypeNode *return_type = nullptr; + SuiteNode *body = nullptr; + bool is_static = false; + bool is_coroutine = false; + MultiplayerAPI::RPCMode rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; + MethodInfo info; + + bool resolved_signature = false; + bool resolved_body = false; + + FunctionNode() { + type = FUNCTION; } }; - struct PatternNode : public Node { - enum PatternType { - PT_CONSTANT, - PT_BIND, - PT_DICTIONARY, - PT_ARRAY, - PT_IGNORE_REST, - PT_WILDCARD + struct GetNodeNode : public ExpressionNode { + LiteralNode *string = nullptr; + Vector<IdentifierNode *> chain; + + GetNodeNode() { + type = GET_NODE; + } + }; + + struct IdentifierNode : public ExpressionNode { + StringName name; + + enum Source { + UNDEFINED_SOURCE, + FUNCTION_PARAMETER, + LOCAL_CONSTANT, + LOCAL_VARIABLE, + LOCAL_ITERATOR, // `for` loop iterator. + LOCAL_BIND, // Pattern bind. + MEMBER_VARIABLE, + MEMBER_CONSTANT, }; + Source source = UNDEFINED_SOURCE; - PatternType pt_type; + union { + ParameterNode *parameter_source = nullptr; + ConstantNode *constant_source; + VariableNode *variable_source; + IdentifierNode *bind_source; + }; - Node *constant; - StringName bind; - Map<ConstantNode *, PatternNode *> dictionary; - Vector<PatternNode *> array; + int usages = 0; // Useful for binds/iterator variable. + + IdentifierNode() { + type = IDENTIFIER; + } }; - struct PatternBranchNode : public Node { - Vector<PatternNode *> patterns; - BlockNode *body; + struct IfNode : public Node { + ExpressionNode *condition = nullptr; + SuiteNode *true_block = nullptr; + SuiteNode *false_block = nullptr; + + IfNode() { + type = IF; + } + }; + + struct LiteralNode : public ExpressionNode { + Variant value; + + LiteralNode() { + type = LITERAL; + } }; struct MatchNode : public Node { - Node *val_to_match; - Vector<PatternBranchNode *> branches; + ExpressionNode *test = nullptr; + Vector<MatchBranchNode *> branches; - struct CompiledPatternBranch { - Node *compiled_pattern; - BlockNode *body; - }; + MatchNode() { + type = MATCH; + } + }; - Vector<CompiledPatternBranch> compiled_pattern_branches; + struct MatchBranchNode : public Node { + Vector<PatternNode *> patterns; + SuiteNode *block; + bool has_wildcard = false; + + MatchBranchNode() { + type = MATCH_BRANCH; + } }; - struct ControlFlowNode : public Node { - enum CFType { - CF_IF, - CF_FOR, - CF_WHILE, - CF_BREAK, - CF_CONTINUE, - CF_RETURN, - CF_MATCH + struct ParameterNode : public Node { + IdentifierNode *identifier = nullptr; + ExpressionNode *default_value = nullptr; + TypeNode *datatype_specifier = nullptr; + bool infer_datatype = false; + int usages = 0; + + ParameterNode() { + type = PARAMETER; + } + }; + + struct PassNode : public Node { + PassNode() { + type = PASS; + } + }; + + struct PatternNode : public Node { + enum Type { + PT_LITERAL, + PT_EXPRESSION, + PT_BIND, + PT_ARRAY, + PT_DICTIONARY, + PT_REST, + PT_WILDCARD, }; + Type pattern_type = PT_LITERAL; - CFType cf_type = CF_IF; - Vector<Node *> arguments; - BlockNode *body = nullptr; - BlockNode *body_else = nullptr; + union { + LiteralNode *literal = nullptr; + IdentifierNode *bind; + ExpressionNode *expression; + }; + Vector<PatternNode *> array; + bool rest_used = false; // For array/dict patterns. - MatchNode *match; + struct Pair { + ExpressionNode *key = nullptr; + PatternNode *value_pattern = nullptr; + }; + Vector<Pair> dictionary; - ControlFlowNode *_else; //used for if + HashMap<StringName, IdentifierNode *> binds; - ControlFlowNode() { - type = TYPE_CONTROL_FLOW; + bool has_bind(const StringName &p_name); + IdentifierNode *get_bind(const StringName &p_name); + + PatternNode() { + type = PATTERN; } }; + struct PreloadNode : public ExpressionNode { + ExpressionNode *path = nullptr; + String resolved_path; + Ref<Resource> resource; - struct CastNode : public Node { - Node *source_node; - DataType cast_type; - DataType return_type; - virtual DataType get_datatype() const { return return_type; } - virtual void set_datatype(const DataType &p_datatype) { return_type = p_datatype; } - - CastNode() { - type = TYPE_CAST; + PreloadNode() { + type = PRELOAD; } }; - struct AssertNode : public Node { - Node *condition = nullptr; - Node *message = nullptr; + struct ReturnNode : public Node { + ExpressionNode *return_value = nullptr; - AssertNode() { - type = TYPE_ASSERT; + ReturnNode() { + type = RETURN; } }; - struct BreakpointNode : public Node { - BreakpointNode() { - type = TYPE_BREAKPOINT; + struct SelfNode : public ExpressionNode { + ClassNode *current_class = nullptr; + + SelfNode() { + type = SELF; } }; - struct NewLineNode : public Node { - NewLineNode() { - type = TYPE_NEWLINE; + struct SignalNode : public Node { + IdentifierNode *identifier = nullptr; + Vector<ParameterNode *> parameters; + HashMap<StringName, int> parameters_indices; + + SignalNode() { + type = SIGNAL; } }; - struct Expression { - bool is_op; + struct SubscriptNode : public ExpressionNode { + ExpressionNode *base = nullptr; union { - OperatorNode::Operator op; - Node *node; + ExpressionNode *index = nullptr; + IdentifierNode *attribute; }; - }; - enum CompletionType { - COMPLETION_NONE, - COMPLETION_BUILT_IN_TYPE_CONSTANT, - COMPLETION_GET_NODE, - COMPLETION_FUNCTION, - COMPLETION_IDENTIFIER, - COMPLETION_PARENT_FUNCTION, - COMPLETION_METHOD, - COMPLETION_CALL_ARGUMENTS, - COMPLETION_RESOURCE_PATH, - COMPLETION_INDEX, - COMPLETION_VIRTUAL_FUNC, - COMPLETION_YIELD, - COMPLETION_ASSIGN, - COMPLETION_TYPE_HINT, - COMPLETION_TYPE_HINT_INDEX, + bool is_attribute = false; + + SubscriptNode() { + type = SUBSCRIPT; + } }; -private: - GDScriptTokenizer *tokenizer; + struct SuiteNode : public Node { + SuiteNode *parent_block = nullptr; + Vector<Node *> statements; + struct Local { + enum Type { + UNDEFINED, + CONSTANT, + VARIABLE, + PARAMETER, + FOR_VARIABLE, + PATTERN_BIND, + }; + Type type = UNDEFINED; + union { + ConstantNode *constant = nullptr; + VariableNode *variable; + ParameterNode *parameter; + IdentifierNode *bind; + }; + StringName name; - Node *head; - Node *list; - template <class T> - T *alloc_node(); - - bool validating; - bool for_completion; - int parenthesis; - bool error_set; - String error; - int error_line; - int error_column; - bool check_types; - bool dependencies_only; - List<String> dependencies; -#ifdef DEBUG_ENABLED - Set<int> *safe_lines; -#endif // DEBUG_ENABLED + int start_line = 0, end_line = 0; + int start_column = 0, end_column = 0; + int leftmost_column = 0, rightmost_column = 0; + + DataType get_datatype() const; + String get_name() const; + + Local() {} + Local(ConstantNode *p_constant) { + type = CONSTANT; + constant = p_constant; + name = p_constant->identifier->name; + + start_line = p_constant->start_line; + end_line = p_constant->end_line; + start_column = p_constant->start_column; + end_column = p_constant->end_column; + leftmost_column = p_constant->leftmost_column; + rightmost_column = p_constant->rightmost_column; + } + Local(VariableNode *p_variable) { + type = VARIABLE; + variable = p_variable; + name = p_variable->identifier->name; + + start_line = p_variable->start_line; + end_line = p_variable->end_line; + start_column = p_variable->start_column; + end_column = p_variable->end_column; + leftmost_column = p_variable->leftmost_column; + rightmost_column = p_variable->rightmost_column; + } + Local(ParameterNode *p_parameter) { + type = PARAMETER; + parameter = p_parameter; + name = p_parameter->identifier->name; + + start_line = p_parameter->start_line; + end_line = p_parameter->end_line; + start_column = p_parameter->start_column; + end_column = p_parameter->end_column; + leftmost_column = p_parameter->leftmost_column; + rightmost_column = p_parameter->rightmost_column; + } + Local(IdentifierNode *p_identifier) { + type = FOR_VARIABLE; + bind = p_identifier; + name = p_identifier->name; + + start_line = p_identifier->start_line; + end_line = p_identifier->end_line; + start_column = p_identifier->start_column; + end_column = p_identifier->end_column; + leftmost_column = p_identifier->leftmost_column; + rightmost_column = p_identifier->rightmost_column; + } + }; + Local empty; + Vector<Local> locals; + HashMap<StringName, int> locals_indices; -#ifdef DEBUG_ENABLED - List<GDScriptWarning> warnings; -#endif // DEBUG_ENABLED + FunctionNode *parent_function = nullptr; + ForNode *parent_for = nullptr; + IfNode *parent_if = nullptr; + + bool has_return = false; + bool has_continue = false; + bool has_unreachable_code = false; // Just so warnings aren't given more than once per block. + + bool has_local(const StringName &p_name) const; + const Local &get_local(const StringName &p_name) const; + template <class T> + void add_local(T *p_local) { + locals_indices[p_local->identifier->name] = locals.size(); + locals.push_back(Local(p_local)); + } + void add_local(const Local &p_local) { + locals_indices[p_local.name] = locals.size(); + locals.push_back(p_local); + } - int pending_newline; + SuiteNode() { + type = SUITE; + } + }; - struct IndentLevel { - int indent = 0; - int tabs = 0; + struct TernaryOpNode : public ExpressionNode { + // Only one ternary operation exists, so no abstraction here. + ExpressionNode *condition = nullptr; + ExpressionNode *true_expr = nullptr; + ExpressionNode *false_expr = nullptr; - bool is_mixed(IndentLevel other) { - return ( - (indent == other.indent && tabs != other.tabs) || - (indent > other.indent && tabs < other.tabs) || - (indent < other.indent && tabs > other.tabs)); + TernaryOpNode() { + type = TERNARY_OPERATOR; } + }; - IndentLevel() {} + struct TypeNode : public Node { + Vector<IdentifierNode *> type_chain; - IndentLevel(int p_indent, int p_tabs) : - indent(p_indent), - tabs(p_tabs) {} + TypeNode() { + type = TYPE; + } }; - List<IndentLevel> indent_level; + struct UnaryOpNode : public ExpressionNode { + enum OpType { + OP_POSITIVE, + OP_NEGATIVE, + OP_COMPLEMENT, + OP_LOGIC_NOT, + }; - String base_path; - String self_path; + OpType operation; + Variant::Operator variant_op = Variant::OP_MAX; + ExpressionNode *operand = nullptr; - ClassNode *current_class; - FunctionNode *current_function; - BlockNode *current_block; + UnaryOpNode() { + type = UNARY_OPERATOR; + } + }; - bool _get_completable_identifier(CompletionType p_type, StringName &identifier); - void _make_completable_call(int p_arg); + struct VariableNode : public Node { + enum PropertyStyle { + PROP_NONE, + PROP_INLINE, + PROP_SETGET, + }; - CompletionType completion_type; - StringName completion_cursor; - Variant::Type completion_built_in_constant; - Node *completion_node; - ClassNode *completion_class; - FunctionNode *completion_function; - BlockNode *completion_block; - int completion_line; - int completion_argument; - bool completion_found; - bool completion_ident_is_call; + IdentifierNode *identifier = nullptr; + ExpressionNode *initializer = nullptr; + TypeNode *datatype_specifier = nullptr; + bool infer_datatype = false; - PropertyInfo current_export; + PropertyStyle property = PROP_NONE; + union { + SuiteNode *setter = nullptr; + IdentifierNode *setter_pointer; + }; + IdentifierNode *setter_parameter = nullptr; + union { + SuiteNode *getter = nullptr; + IdentifierNode *getter_pointer; + }; - MultiplayerAPI::RPCMode rpc_mode; + bool exported = false; + bool onready = false; + PropertyInfo export_info; + MultiplayerAPI::RPCMode rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED; + int assignments = 0; + int usages = 0; - void _set_error(const String &p_error, int p_line = -1, int p_column = -1); -#ifdef DEBUG_ENABLED - void _add_warning(int p_code, int p_line = -1, const String &p_symbol1 = String(), const String &p_symbol2 = String(), const String &p_symbol3 = String(), const String &p_symbol4 = String()); - void _add_warning(int p_code, int p_line, const Vector<String> &p_symbols); -#endif // DEBUG_ENABLED - bool _recover_from_completion(); - - bool _parse_arguments(Node *p_parent, Vector<Node *> &p_args, bool p_static, bool p_can_codecomplete = false, bool p_parsing_constant = false); - bool _enter_indent_block(BlockNode *p_block = nullptr); - bool _parse_newline(); - Node *_parse_expression(Node *p_parent, bool p_static, bool p_allow_assign = false, bool p_parsing_constant = false); - Node *_reduce_expression(Node *p_node, bool p_to_const = false); - Node *_parse_and_reduce_expression(Node *p_parent, bool p_static, bool p_reduce_const = false, bool p_allow_assign = false); - bool _reduce_export_var_type(Variant &p_value, int p_line = 0); - - PatternNode *_parse_pattern(bool p_static); - void _parse_pattern_block(BlockNode *p_block, Vector<PatternBranchNode *> &p_branches, bool p_static); - void _transform_match_statment(MatchNode *p_match_statement); - void _generate_pattern(PatternNode *p_pattern, Node *p_node_to_match, Node *&p_resulting_node, Map<StringName, Node *> &p_bindings); - - void _parse_block(BlockNode *p_block, bool p_static); - void _parse_extends(ClassNode *p_class); - void _parse_class(ClassNode *p_class); - bool _end_statement(); - void _set_end_statement_error(String p_name); - - void _determine_inheritance(ClassNode *p_class, bool p_recursive = true); - bool _parse_type(DataType &r_type, bool p_can_be_void = false); - DataType _resolve_type(const DataType &p_source, int p_line); - DataType _type_from_variant(const Variant &p_value) const; - DataType _type_from_property(const PropertyInfo &p_property, bool p_nil_is_variant = true) const; - DataType _type_from_gdtype(const GDScriptDataType &p_gdtype) const; - DataType _get_operation_type(const Variant::Operator p_op, const DataType &p_a, const DataType &p_b, bool &r_valid) const; - Variant::Operator _get_variant_operation(const OperatorNode::Operator &p_op) const; - bool _get_function_signature(DataType &p_base_type, const StringName &p_function, DataType &r_return_type, List<DataType> &r_arg_types, int &r_default_arg_count, bool &r_static, bool &r_vararg) const; - bool _get_member_type(const DataType &p_base_type, const StringName &p_member, DataType &r_member_type, bool *r_is_const = nullptr) const; - bool _is_type_compatible(const DataType &p_container, const DataType &p_expression, bool p_allow_implicit_conversion = false) const; - Node *_get_default_value_for_type(const DataType &p_type, int p_line = -1); - - DataType _reduce_node_type(Node *p_node); - DataType _reduce_function_call_type(const OperatorNode *p_call); - DataType _reduce_identifier_type(const DataType *p_base_type, const StringName &p_identifier, int p_line, bool p_is_indexing); - void _check_class_level_types(ClassNode *p_class); - void _check_class_blocks_types(ClassNode *p_class); - void _check_function_types(FunctionNode *p_function); - void _check_block_types(BlockNode *p_block); - _FORCE_INLINE_ void _mark_line_as_safe(int p_line) const { -#ifdef DEBUG_ENABLED - if (safe_lines) { - safe_lines->insert(p_line); + VariableNode() { + type = VARIABLE; } -#endif // DEBUG_ENABLED - } - _FORCE_INLINE_ void _mark_line_as_unsafe(int p_line) const { -#ifdef DEBUG_ENABLED - if (safe_lines) { - safe_lines->erase(p_line); + }; + + struct WhileNode : public Node { + ExpressionNode *condition = nullptr; + SuiteNode *loop = nullptr; + + WhileNode() { + type = WHILE; } -#endif // DEBUG_ENABLED - } + }; - Error _parse(const String &p_base_path); + enum CompletionType { + COMPLETION_NONE, + COMPLETION_ANNOTATION, // Annotation (following @). + COMPLETION_ANNOTATION_ARGUMENTS, // Annotation arguments hint. + COMPLETION_ASSIGN, // Assignment based on type (e.g. enum values). + COMPLETION_ATTRIBUTE, // After id.| to look for members. + COMPLETION_ATTRIBUTE_METHOD, // After id.| to look for methods. + COMPLETION_BUILT_IN_TYPE_CONSTANT, // Constants inside a built-in type (e.g. Color.blue). + COMPLETION_CALL_ARGUMENTS, // Complete with nodes, input actions, enum values (or usual expressions). + // TODO: COMPLETION_DECLARATION, // Potential declaration (var, const, func). + COMPLETION_GET_NODE, // Get node with $ notation. + COMPLETION_IDENTIFIER, // List available identifiers in scope. + COMPLETION_INHERIT_TYPE, // Type after extends. Exclude non-viable types (built-ins, enums, void). Includes subtypes using the argument index. + COMPLETION_METHOD, // List available methods in scope. + COMPLETION_OVERRIDE_METHOD, // Override implementation, also for native virtuals. + COMPLETION_PROPERTY_DECLARATION, // Property declaration (get, set). + COMPLETION_PROPERTY_DECLARATION_OR_TYPE, // Property declaration (get, set) or a type hint. + COMPLETION_PROPERTY_METHOD, // Property setter or getter (list available methods). + COMPLETION_RESOURCE_PATH, // For load/preload. + COMPLETION_SUBSCRIPT, // Inside id[|]. + COMPLETION_SUPER_METHOD, // After super. + COMPLETION_TYPE_ATTRIBUTE, // Attribute in type name (Type.|). + COMPLETION_TYPE_NAME, // Name of type (after :). + COMPLETION_TYPE_NAME_OR_VOID, // Same as TYPE_NAME, but allows void (in function return type). + }; -public: - bool has_error() const; - String get_error() const; - int get_error_line() const; - int get_error_column() const; + struct CompletionContext { + CompletionType type = COMPLETION_NONE; + ClassNode *current_class = nullptr; + FunctionNode *current_function = nullptr; + SuiteNode *current_suite = nullptr; + int current_line = -1; + int current_argument = -1; + Variant::Type builtin_type = Variant::VARIANT_MAX; + Node *node = nullptr; + Object *base = nullptr; + List<Ref<GDScriptParserRef>> dependent_parsers; + }; + + struct CompletionCall { + Node *call = nullptr; + int argument = -1; + }; + +private: + friend class GDScriptAnalyzer; + + bool _is_tool = false; + String script_path; + bool for_completion = false; + bool panic_mode = false; + bool can_break = false; + bool can_continue = false; + bool is_ignoring_warnings = false; + List<bool> multiline_stack; + + ClassNode *head = nullptr; + Node *list = nullptr; + List<ParserError> errors; #ifdef DEBUG_ENABLED - const List<GDScriptWarning> &get_warnings() const { return warnings; } -#endif // DEBUG_ENABLED - Error parse(const String &p_code, const String &p_base_path = "", bool p_just_validate = false, const String &p_self_path = "", bool p_for_completion = false, Set<int> *r_safe_lines = nullptr, bool p_dependencies_only = false); - Error parse_bytecode(const Vector<uint8_t> &p_bytecode, const String &p_base_path = "", const String &p_self_path = ""); + List<GDScriptWarning> warnings; + Set<String> ignored_warnings; + Set<int> unsafe_lines; +#endif - bool is_tool_script() const; - const Node *get_parse_tree() const; + GDScriptTokenizer tokenizer; + GDScriptTokenizer::Token previous; + GDScriptTokenizer::Token current; + + ClassNode *current_class = nullptr; + FunctionNode *current_function = nullptr; + SuiteNode *current_suite = nullptr; + + CompletionContext completion_context; + CompletionCall completion_call; + List<CompletionCall> completion_call_stack; + bool passed_cursor = false; + + typedef bool (GDScriptParser::*AnnotationAction)(const AnnotationNode *p_annotation, Node *p_target); + struct AnnotationInfo { + enum TargetKind { + NONE = 0, + SCRIPT = 1 << 0, + CLASS = 1 << 1, + VARIABLE = 1 << 2, + CONSTANT = 1 << 3, + SIGNAL = 1 << 4, + FUNCTION = 1 << 5, + STATEMENT = 1 << 6, + CLASS_LEVEL = CLASS | VARIABLE | FUNCTION, + }; + uint32_t target_kind = 0; // Flags. + AnnotationAction apply = nullptr; + MethodInfo info; + }; + HashMap<StringName, AnnotationInfo> valid_annotations; + List<AnnotationNode *> annotation_stack; + + typedef ExpressionNode *(GDScriptParser::*ParseFunction)(ExpressionNode *p_previous_operand, bool p_can_assign); + // Higher value means higher precedence (i.e. is evaluated first). + enum Precedence { + PREC_NONE, + PREC_ASSIGNMENT, + PREC_CAST, + PREC_TERNARY, + PREC_LOGIC_OR, + PREC_LOGIC_AND, + PREC_LOGIC_NOT, + PREC_CONTENT_TEST, + PREC_COMPARISON, + PREC_BIT_OR, + PREC_BIT_XOR, + PREC_BIT_AND, + PREC_BIT_SHIFT, + PREC_SUBTRACTION, + PREC_ADDITION, + PREC_FACTOR, + PREC_SIGN, + PREC_BIT_NOT, + PREC_TYPE_TEST, + PREC_AWAIT, + PREC_CALL, + PREC_ATTRIBUTE, + PREC_SUBSCRIPT, + PREC_PRIMARY, + }; + struct ParseRule { + ParseFunction prefix = nullptr; + ParseFunction infix = nullptr; + Precedence precedence = PREC_NONE; + }; + static ParseRule *get_rule(GDScriptTokenizer::Token::Type p_token_type); - //completion info + template <class T> + T *alloc_node() { + T *node = memnew(T); - CompletionType get_completion_type(); - StringName get_completion_cursor(); - int get_completion_line(); - Variant::Type get_completion_built_in_constant(); - Node *get_completion_node(); - ClassNode *get_completion_class(); - BlockNode *get_completion_block(); - FunctionNode *get_completion_function(); - int get_completion_argument_index(); - bool get_completion_identifier_is_function(); + node->next = list; + list = node; - const List<String> &get_dependencies() const { return dependencies; } + // TODO: Properly set positions for all nodes. + node->start_line = previous.start_line; + node->end_line = previous.end_line; + node->start_column = previous.start_column; + node->end_column = previous.end_column; + node->leftmost_column = previous.leftmost_column; + node->rightmost_column = previous.rightmost_column; + return node; + } void clear(); + void push_error(const String &p_message, const Node *p_origin = nullptr); +#ifdef DEBUG_ENABLED + void push_warning(const Node *p_source, GDScriptWarning::Code p_code, const String &p_symbol1 = String(), const String &p_symbol2 = String(), const String &p_symbol3 = String(), const String &p_symbol4 = String()); + void push_warning(const Node *p_source, GDScriptWarning::Code p_code, const Vector<String> &p_symbols); +#endif + + void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1, bool p_force = false); + void make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force = false); + void push_completion_call(Node *p_call); + void pop_completion_call(); + void set_last_completion_call_arg(int p_argument); + + GDScriptTokenizer::Token advance(); + bool match(GDScriptTokenizer::Token::Type p_token_type); + bool check(GDScriptTokenizer::Token::Type p_token_type); + bool consume(GDScriptTokenizer::Token::Type p_token_type, const String &p_error_message); + bool is_at_end(); + bool is_statement_end(); + void end_statement(const String &p_context); + void synchronize(); + void push_multiline(bool p_state); + void pop_multiline(); + + // Main blocks. + void parse_program(); + ClassNode *parse_class(); + void parse_class_name(); + void parse_extends(); + void parse_class_body(); + template <class T> + void parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind); + SignalNode *parse_signal(); + EnumNode *parse_enum(); + ParameterNode *parse_parameter(); + FunctionNode *parse_function(); + SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr); + // Annotations + AnnotationNode *parse_annotation(uint32_t p_valid_targets); + bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, int p_optional_arguments = 0, bool p_is_vararg = false); + bool validate_annotation_arguments(AnnotationNode *p_annotation); + void clear_unused_annotations(); + bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target); + bool icon_annotation(const AnnotationNode *p_annotation, Node *p_target); + bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target); + template <PropertyHint t_hint, Variant::Type t_type> + bool export_annotations(const AnnotationNode *p_annotation, Node *p_target); + bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target); + template <MultiplayerAPI::RPCMode t_mode> + bool network_annotations(const AnnotationNode *p_annotation, Node *p_target); + // Statements. + Node *parse_statement(); + VariableNode *parse_variable(); + VariableNode *parse_variable(bool p_allow_property); + VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent); + void parse_property_getter(VariableNode *p_variable); + void parse_property_setter(VariableNode *p_variable); + ConstantNode *parse_constant(); + AssertNode *parse_assert(); + BreakNode *parse_break(); + ContinueNode *parse_continue(); + ForNode *parse_for(); + IfNode *parse_if(const String &p_token = "if"); + MatchNode *parse_match(); + MatchBranchNode *parse_match_branch(); + PatternNode *parse_match_pattern(PatternNode *p_root_pattern = nullptr); + WhileNode *parse_while(); + // Expressions. + ExpressionNode *parse_expression(bool p_can_assign, bool p_stop_on_assign = false); + ExpressionNode *parse_precedence(Precedence p_precedence, bool p_can_assign, bool p_stop_on_assign = false); + ExpressionNode *parse_literal(ExpressionNode *p_previous_operand, bool p_can_assign); + LiteralNode *parse_literal(); + ExpressionNode *parse_self(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_identifier(ExpressionNode *p_previous_operand, bool p_can_assign); + IdentifierNode *parse_identifier(); + ExpressionNode *parse_builtin_constant(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_unary_operator(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_binary_operator(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_ternary_operator(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_assignment(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_array(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_dictionary(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_call(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_get_node(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_preload(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_grouping(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_cast(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_await(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_attribute(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_subscript(ExpressionNode *p_previous_operand, bool p_can_assign); + ExpressionNode *parse_invalid_token(ExpressionNode *p_previous_operand, bool p_can_assign); + TypeNode *parse_type(bool p_allow_void = false); + +public: + Error parse(const String &p_source_code, const String &p_script_path, bool p_for_completion); + ClassNode *get_tree() const { return head; } + bool is_tool() const { return _is_tool; } + static Variant::Type get_builtin_type(const StringName &p_type); + static GDScriptFunctions::Function get_builtin_function(const StringName &p_name); + + CompletionContext get_completion_context() const { return completion_context; } + CompletionCall get_completion_call() const { return completion_call; } + void get_annotation_list(List<MethodInfo> *r_annotations) const; + + const List<ParserError> &get_errors() const { return errors; } + const List<String> get_dependencies() const { + // TODO: Keep track of deps. + return List<String>(); + } +#ifdef DEBUG_ENABLED + const List<GDScriptWarning> &get_warnings() const { return warnings; } + const Set<int> &get_unsafe_lines() const { return unsafe_lines; } + int get_last_line_number() const { return current.end_line; } +#endif + GDScriptParser(); ~GDScriptParser(); + +#ifdef DEBUG_ENABLED + class TreePrinter { + int indent_level = 0; + String indent; + StringBuilder printed; + bool pending_indent = false; + + void increase_indent(); + void decrease_indent(); + void push_line(const String &p_line = String()); + void push_text(const String &p_text); + + void print_annotation(AnnotationNode *p_annotation); + void print_array(ArrayNode *p_array); + void print_assert(AssertNode *p_assert); + void print_assignment(AssignmentNode *p_assignment); + void print_await(AwaitNode *p_await); + void print_binary_op(BinaryOpNode *p_binary_op); + void print_call(CallNode *p_call); + void print_cast(CastNode *p_cast); + void print_class(ClassNode *p_class); + void print_constant(ConstantNode *p_constant); + void print_dictionary(DictionaryNode *p_dictionary); + void print_expression(ExpressionNode *p_expression); + void print_enum(EnumNode *p_enum); + void print_for(ForNode *p_for); + void print_function(FunctionNode *p_function); + void print_get_node(GetNodeNode *p_get_node); + void print_if(IfNode *p_if, bool p_is_elif = false); + void print_identifier(IdentifierNode *p_identifier); + void print_literal(LiteralNode *p_literal); + void print_match(MatchNode *p_match); + void print_match_branch(MatchBranchNode *p_match_branch); + void print_match_pattern(PatternNode *p_match_pattern); + void print_parameter(ParameterNode *p_parameter); + void print_preload(PreloadNode *p_preload); + void print_return(ReturnNode *p_return); + void print_self(SelfNode *p_self); + void print_signal(SignalNode *p_signal); + void print_statement(Node *p_statement); + void print_subscript(SubscriptNode *p_subscript); + void print_suite(SuiteNode *p_suite); + void print_type(TypeNode *p_type); + void print_ternary_op(TernaryOpNode *p_ternary_op); + void print_unary_op(UnaryOpNode *p_unary_op); + void print_variable(VariableNode *p_variable); + void print_while(WhileNode *p_while); + + public: + void print_tree(const GDScriptParser &p_parser); + }; +#endif // DEBUG_ENABLED + static void cleanup(); }; #endif // GDSCRIPT_PARSER_H diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index 82def3f877..737e920693 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -30,1476 +30,1352 @@ #include "gdscript_tokenizer.h" -#include "core/io/marshalls.h" -#include "core/map.h" -#include "core/print_string.h" -#include "gdscript_functions.h" - -const char *GDScriptTokenizer::token_names[TK_MAX] = { - "Empty", - "Identifier", - "Constant", - "Self", - "Built-In Type", - "Built-In Func", - "In", - "'=='", - "'!='", - "'<'", - "'<='", - "'>'", - "'>='", - "'and'", - "'or'", - "'not'", - "'+'", - "'-'", - "'*'", - "'/'", - "'%'", - "'<<'", - "'>>'", - "'='", - "'+='", - "'-='", - "'*='", - "'/='", - "'%='", - "'<<='", - "'>>='", - "'&='", - "'|='", - "'^='", - "'&'", - "'|'", - "'^'", - "'~'", - //"Plus Plus", - //"Minus Minus", - "if", - "elif", - "else", - "for", - "while", - "break", - "continue", - "pass", - "return", - "match", - "func", - "class", - "class_name", - "extends", - "is", - "onready", - "tool", - "static", - "export", - "setget", - "const", - "var", - "as", - "void", - "enum", - "preload", - "assert", - "yield", - "signal", - "breakpoint", - "remote", - "master", - "puppet", - "remotesync", - "mastersync", - "puppetsync", - "'['", - "']'", - "'{'", - "'}'", - "'('", - "')'", - "','", - "';'", - "'.'", - "'?'", - "':'", - "'$'", - "'->'", - "'\\n'", - "PI", - "TAU", - "_", - "INF", - "NAN", - "Error", - "EOF", - "Cursor" +#include "core/error_macros.h" + +#ifdef TOOLS_ENABLED +#include "editor/editor_settings.h" +#endif + +static const char *token_names[] = { + "Empty", // EMPTY, + // Basic + "Annotation", // ANNOTATION + "Identifier", // IDENTIFIER, + "Literal", // LITERAL, + // Comparison + "<", // LESS, + "<=", // LESS_EQUAL, + ">", // GREATER, + ">=", // GREATER_EQUAL, + "==", // EQUAL_EQUAL, + "!=", // BANG_EQUAL, + // Logical + "and", // AND, + "or", // OR, + "not", // NOT, + "&&", // AMPERSAND_AMPERSAND, + "||", // PIPE_PIPE, + "!", // BANG, + // Bitwise + "&", // AMPERSAND, + "|", // PIPE, + "~", // TILDE, + "^", // CARET, + "<<", // LESS_LESS, + ">>", // GREATER_GREATER, + // Math + "+", // PLUS, + "-", // MINUS, + "*", // STAR, + "/", // SLASH, + "%", // PERCENT, + // Assignment + "=", // EQUAL, + "+=", // PLUS_EQUAL, + "-=", // MINUS_EQUAL, + "*=", // STAR_EQUAL, + "/=", // SLASH_EQUAL, + "%=", // PERCENT_EQUAL, + "<<=", // LESS_LESS_EQUAL, + ">>=", // GREATER_GREATER_EQUAL, + "&=", // AMPERSAND_EQUAL, + "|=", // PIPE_EQUAL, + "^=", // CARET_EQUAL, + // Control flow + "if", // IF, + "elif", // ELIF, + "else", // ELSE, + "for", // FOR, + "while", // WHILE, + "break", // BREAK, + "continue", // CONTINUE, + "pass", // PASS, + "return", // RETURN, + "match", // MATCH, + // Keywords + "as", // AS, + "assert", // ASSERT, + "await", // AWAIT, + "breakpoint", // BREAKPOINT, + "class", // CLASS, + "class_name", // CLASS_NAME, + "const", // CONST, + "enum", // ENUM, + "extends", // EXTENDS, + "func", // FUNC, + "in", // IN, + "is", // IS, + "namespace", // NAMESPACE + "preload", // PRELOAD, + "self", // SELF, + "signal", // SIGNAL, + "static", // STATIC, + "super", // SUPER, + "trait", // TRAIT, + "var", // VAR, + "void", // VOID, + "yield", // YIELD, + // Punctuation + "[", // BRACKET_OPEN, + "]", // BRACKET_CLOSE, + "{", // BRACE_OPEN, + "}", // BRACE_CLOSE, + "(", // PARENTHESIS_OPEN, + ")", // PARENTHESIS_CLOSE, + ",", // COMMA, + ";", // SEMICOLON, + ".", // PERIOD, + "..", // PERIOD_PERIOD, + ":", // COLON, + "$", // DOLLAR, + "->", // FORWARD_ARROW, + "_", // UNDERSCORE, + // Whitespace + "Newline", // NEWLINE, + "Indent", // INDENT, + "Dedent", // DEDENT, + // Constants + "PI", // CONST_PI, + "TAU", // CONST_TAU, + "INF", // CONST_INF, + "NaN", // CONST_NAN, + // Error message improvement + "VCS conflict marker", // VCS_CONFLICT_MARKER, + "`", // BACKTICK, + "?", // QUESTION_MARK, + // Special + "Error", // ERROR, + "End of file", // EOF, }; -struct _bit { - Variant::Type type; - const char *text; -}; -//built in types - -static const _bit _type_list[] = { - //types - { Variant::BOOL, "bool" }, - { Variant::INT, "int" }, - { Variant::FLOAT, "float" }, - { Variant::STRING, "String" }, - { Variant::VECTOR2, "Vector2" }, - { Variant::VECTOR2I, "Vector2i" }, - { Variant::RECT2, "Rect2" }, - { Variant::RECT2I, "Rect2i" }, - { Variant::TRANSFORM2D, "Transform2D" }, - { Variant::VECTOR3, "Vector3" }, - { Variant::VECTOR3I, "Vector3i" }, - { Variant::AABB, "AABB" }, - { Variant::PLANE, "Plane" }, - { Variant::QUAT, "Quat" }, - { Variant::BASIS, "Basis" }, - { Variant::TRANSFORM, "Transform" }, - { Variant::COLOR, "Color" }, - { Variant::_RID, "RID" }, - { Variant::OBJECT, "Object" }, - { Variant::STRING_NAME, "StringName" }, - { Variant::NODE_PATH, "NodePath" }, - { Variant::DICTIONARY, "Dictionary" }, - { Variant::CALLABLE, "Callable" }, - { Variant::SIGNAL, "Signal" }, - { Variant::ARRAY, "Array" }, - { Variant::PACKED_BYTE_ARRAY, "PackedByteArray" }, - { Variant::PACKED_INT32_ARRAY, "PackedInt32Array" }, - { Variant::PACKED_INT64_ARRAY, "PackedInt64Array" }, - { Variant::PACKED_FLOAT32_ARRAY, "PackedFloat32Array" }, - { Variant::PACKED_FLOAT64_ARRAY, "PackedFloat64Array" }, - { Variant::PACKED_STRING_ARRAY, "PackedStringArray" }, - { Variant::PACKED_VECTOR2_ARRAY, "PackedVector2Array" }, - { Variant::PACKED_VECTOR3_ARRAY, "PackedVector3Array" }, - { Variant::PACKED_COLOR_ARRAY, "PackedColorArray" }, - { Variant::VARIANT_MAX, nullptr }, -}; - -struct _kws { - GDScriptTokenizer::Token token; - const char *text; -}; - -static const _kws _keyword_list[] = { - //ops - { GDScriptTokenizer::TK_OP_IN, "in" }, - { GDScriptTokenizer::TK_OP_NOT, "not" }, - { GDScriptTokenizer::TK_OP_OR, "or" }, - { GDScriptTokenizer::TK_OP_AND, "and" }, - //func - { GDScriptTokenizer::TK_PR_FUNCTION, "func" }, - { GDScriptTokenizer::TK_PR_CLASS, "class" }, - { GDScriptTokenizer::TK_PR_CLASS_NAME, "class_name" }, - { GDScriptTokenizer::TK_PR_EXTENDS, "extends" }, - { GDScriptTokenizer::TK_PR_IS, "is" }, - { GDScriptTokenizer::TK_PR_ONREADY, "onready" }, - { GDScriptTokenizer::TK_PR_TOOL, "tool" }, - { GDScriptTokenizer::TK_PR_STATIC, "static" }, - { GDScriptTokenizer::TK_PR_EXPORT, "export" }, - { GDScriptTokenizer::TK_PR_SETGET, "setget" }, - { GDScriptTokenizer::TK_PR_VAR, "var" }, - { GDScriptTokenizer::TK_PR_AS, "as" }, - { GDScriptTokenizer::TK_PR_VOID, "void" }, - { GDScriptTokenizer::TK_PR_PRELOAD, "preload" }, - { GDScriptTokenizer::TK_PR_ASSERT, "assert" }, - { GDScriptTokenizer::TK_PR_YIELD, "yield" }, - { GDScriptTokenizer::TK_PR_SIGNAL, "signal" }, - { GDScriptTokenizer::TK_PR_BREAKPOINT, "breakpoint" }, - { GDScriptTokenizer::TK_PR_REMOTE, "remote" }, - { GDScriptTokenizer::TK_PR_MASTER, "master" }, - { GDScriptTokenizer::TK_PR_PUPPET, "puppet" }, - { GDScriptTokenizer::TK_PR_REMOTESYNC, "remotesync" }, - { GDScriptTokenizer::TK_PR_MASTERSYNC, "mastersync" }, - { GDScriptTokenizer::TK_PR_PUPPETSYNC, "puppetsync" }, - { GDScriptTokenizer::TK_PR_CONST, "const" }, - { GDScriptTokenizer::TK_PR_ENUM, "enum" }, - //controlflow - { GDScriptTokenizer::TK_CF_IF, "if" }, - { GDScriptTokenizer::TK_CF_ELIF, "elif" }, - { GDScriptTokenizer::TK_CF_ELSE, "else" }, - { GDScriptTokenizer::TK_CF_FOR, "for" }, - { GDScriptTokenizer::TK_CF_WHILE, "while" }, - { GDScriptTokenizer::TK_CF_BREAK, "break" }, - { GDScriptTokenizer::TK_CF_CONTINUE, "continue" }, - { GDScriptTokenizer::TK_CF_RETURN, "return" }, - { GDScriptTokenizer::TK_CF_MATCH, "match" }, - { GDScriptTokenizer::TK_CF_PASS, "pass" }, - { GDScriptTokenizer::TK_SELF, "self" }, - { GDScriptTokenizer::TK_CONST_PI, "PI" }, - { GDScriptTokenizer::TK_CONST_TAU, "TAU" }, - { GDScriptTokenizer::TK_WILDCARD, "_" }, - { GDScriptTokenizer::TK_CONST_INF, "INF" }, - { GDScriptTokenizer::TK_CONST_NAN, "NAN" }, - { GDScriptTokenizer::TK_ERROR, nullptr } -}; +// Avoid desync. +static_assert(sizeof(token_names) / sizeof(token_names[0]) == GDScriptTokenizer::Token::TK_MAX, "Amount of token names don't match the amount of token types."); -const char *GDScriptTokenizer::get_token_name(Token p_token) { - ERR_FAIL_INDEX_V(p_token, TK_MAX, "<error>"); - return token_names[p_token]; +const char *GDScriptTokenizer::Token::get_name() const { + ERR_FAIL_INDEX_V_MSG(type, TK_MAX, "<error>", "Using token type out of the enum."); + return token_names[type]; } -bool GDScriptTokenizer::is_token_literal(int p_offset, bool variable_safe) const { - switch (get_token(p_offset)) { - // Can always be literal: - case TK_IDENTIFIER: - - case TK_PR_ONREADY: - case TK_PR_TOOL: - case TK_PR_STATIC: - case TK_PR_EXPORT: - case TK_PR_SETGET: - case TK_PR_SIGNAL: - case TK_PR_REMOTE: - case TK_PR_MASTER: - case TK_PR_PUPPET: - case TK_PR_REMOTESYNC: - case TK_PR_MASTERSYNC: - case TK_PR_PUPPETSYNC: +bool GDScriptTokenizer::Token::is_identifier() const { + // Note: Most keywords should not be recognized as identifiers. + // These are only exceptions for stuff that already is on the engine's API. + switch (type) { + case IDENTIFIER: + case MATCH: // Used in String.match(). return true; - - // Literal for non-variables only: - case TK_BUILT_IN_TYPE: - case TK_BUILT_IN_FUNC: - - case TK_OP_IN: - //case TK_OP_NOT: - //case TK_OP_OR: - //case TK_OP_AND: - - case TK_PR_CLASS: - case TK_PR_CONST: - case TK_PR_ENUM: - case TK_PR_PRELOAD: - case TK_PR_FUNCTION: - case TK_PR_EXTENDS: - case TK_PR_ASSERT: - case TK_PR_YIELD: - case TK_PR_VAR: - - case TK_CF_IF: - case TK_CF_ELIF: - case TK_CF_ELSE: - case TK_CF_FOR: - case TK_CF_WHILE: - case TK_CF_BREAK: - case TK_CF_CONTINUE: - case TK_CF_RETURN: - case TK_CF_MATCH: - case TK_CF_PASS: - case TK_SELF: - case TK_CONST_PI: - case TK_CONST_TAU: - case TK_WILDCARD: - case TK_CONST_INF: - case TK_CONST_NAN: - case TK_ERROR: - return !variable_safe; - - case TK_CONSTANT: { - switch (get_token_constant(p_offset).get_type()) { - case Variant::NIL: - case Variant::BOOL: - return true; - default: - return false; - } - } default: return false; } } -StringName GDScriptTokenizer::get_token_literal(int p_offset) const { - Token token = get_token(p_offset); - switch (token) { - case TK_IDENTIFIER: - return get_token_identifier(p_offset); - case TK_BUILT_IN_TYPE: { - Variant::Type type = get_token_type(p_offset); - int idx = 0; - - while (_type_list[idx].text) { - if (type == _type_list[idx].type) { - return _type_list[idx].text; - } - idx++; - } - } break; // Shouldn't get here, stuff happens - case TK_BUILT_IN_FUNC: - return GDScriptFunctions::get_func_name(get_token_built_in_func(p_offset)); - case TK_CONSTANT: { - const Variant value = get_token_constant(p_offset); - - switch (value.get_type()) { - case Variant::NIL: - return "null"; - case Variant::BOOL: - return value ? "true" : "false"; - default: { - } - } - } break; - case TK_OP_AND: - case TK_OP_OR: - break; // Don't get into default, since they can be non-literal - default: { - int idx = 0; - - while (_keyword_list[idx].text) { - if (token == _keyword_list[idx].token) { - return _keyword_list[idx].text; - } - idx++; - } - } +bool GDScriptTokenizer::Token::is_node_name() const { + // This is meant to allow keywords with the $ notation, but not as general identifiers. + switch (type) { + case IDENTIFIER: + case AND: + case AS: + case ASSERT: + case AWAIT: + case BREAK: + case BREAKPOINT: + case CLASS_NAME: + case CLASS: + case CONST: + case CONTINUE: + case ELIF: + case ELSE: + case ENUM: + case EXTENDS: + case FOR: + case FUNC: + case IF: + case IN: + case IS: + case MATCH: + case NAMESPACE: + case NOT: + case OR: + case PASS: + case PRELOAD: + case RETURN: + case SELF: + case SIGNAL: + case STATIC: + case SUPER: + case TRAIT: + case UNDERSCORE: + case VAR: + case VOID: + case WHILE: + case YIELD: + return true; + default: + return false; } - ERR_FAIL_V_MSG("", "Failed to get token literal."); } -static bool _is_text_char(CharType c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +String GDScriptTokenizer::get_token_name(Token::Type p_token_type) { + ERR_FAIL_INDEX_V_MSG(p_token_type, Token::TK_MAX, "<error>", "Using token type out of the enum."); + return token_names[p_token_type]; } -static bool _is_number(CharType c) { - return (c >= '0' && c <= '9'); +void GDScriptTokenizer::set_source_code(const String &p_source_code) { + source = p_source_code; + if (source.empty()) { + _source = L""; + } else { + _source = source.ptr(); + } + _current = _source; + line = 1; + column = 1; + length = p_source_code.length(); + position = 0; } -static bool _is_hex(CharType c) { - return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +void GDScriptTokenizer::set_cursor_position(int p_line, int p_column) { + cursor_line = p_line; + cursor_column = p_column; } -static bool _is_bin(CharType c) { - return (c == '0' || c == '1'); +void GDScriptTokenizer::set_multiline_mode(bool p_state) { + multiline_mode = p_state; } -void GDScriptTokenizerText::_make_token(Token p_type) { - TokenData &tk = tk_rb[tk_rb_pos]; - - tk.type = p_type; - tk.line = line; - tk.col = column; - - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +int GDScriptTokenizer::get_cursor_line() const { + return cursor_line; } -void GDScriptTokenizerText::_make_identifier(const StringName &p_identifier) { - TokenData &tk = tk_rb[tk_rb_pos]; - - tk.type = TK_IDENTIFIER; - tk.identifier = p_identifier; - tk.line = line; - tk.col = column; - - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +int GDScriptTokenizer::get_cursor_column() const { + return cursor_column; } -void GDScriptTokenizerText::_make_built_in_func(GDScriptFunctions::Function p_func) { - TokenData &tk = tk_rb[tk_rb_pos]; - - tk.type = TK_BUILT_IN_FUNC; - tk.func = p_func; - tk.line = line; - tk.col = column; - - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +bool GDScriptTokenizer::is_past_cursor() const { + if (line < cursor_line) { + return false; + } + if (line > cursor_line) { + return true; + } + if (column < cursor_column) { + return false; + } + return true; } -void GDScriptTokenizerText::_make_constant(const Variant &p_constant) { - TokenData &tk = tk_rb[tk_rb_pos]; - - tk.type = TK_CONSTANT; - tk.constant = p_constant; - tk.line = line; - tk.col = column; - - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +CharType GDScriptTokenizer::_advance() { + if (unlikely(_is_at_end())) { + return '\0'; + } + _current++; + column++; + position++; + if (column > rightmost_column) { + rightmost_column = column; + } + if (unlikely(_is_at_end())) { + // Add extra newline even if it's not there, to satisfy the parser. + newline(true); + // Also add needed unindent. + check_indent(); + } + return _peek(-1); } -void GDScriptTokenizerText::_make_type(const Variant::Type &p_type) { - TokenData &tk = tk_rb[tk_rb_pos]; - - tk.type = TK_BUILT_IN_TYPE; - tk.vtype = p_type; - tk.line = line; - tk.col = column; - - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +void GDScriptTokenizer::push_paren(CharType p_char) { + paren_stack.push_back(p_char); } -void GDScriptTokenizerText::_make_error(const String &p_error) { - error_flag = true; - last_error = p_error; +bool GDScriptTokenizer::pop_paren(CharType p_expected) { + if (paren_stack.empty()) { + return false; + } + CharType actual = paren_stack.back()->get(); + paren_stack.pop_back(); - TokenData &tk = tk_rb[tk_rb_pos]; - tk.type = TK_ERROR; - tk.constant = p_error; - tk.line = line; - tk.col = column; - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; + return actual == p_expected; } -void GDScriptTokenizerText::_make_newline(int p_indentation, int p_tabs) { - TokenData &tk = tk_rb[tk_rb_pos]; - tk.type = TK_NEWLINE; - tk.constant = Vector2(p_indentation, p_tabs); - tk.line = line; - tk.col = column; - tk_rb_pos = (tk_rb_pos + 1) % TK_RB_SIZE; +GDScriptTokenizer::Token GDScriptTokenizer::pop_error() { + Token error = error_stack.back()->get(); + error_stack.pop_back(); + return error; } -void GDScriptTokenizerText::_advance() { - if (error_flag) { - //parser broke - _make_error(last_error); - return; - } - - if (code_pos >= len) { - _make_token(TK_EOF); - return; - } -#define GETCHAR(m_ofs) ((m_ofs + code_pos) >= len ? 0 : _code[m_ofs + code_pos]) -#define INCPOS(m_amount) \ - { \ - code_pos += m_amount; \ - column += m_amount; \ - } - while (true) { - bool is_string_name = false; - StringMode string_mode = STRING_DOUBLE_QUOTE; - - switch (GETCHAR(0)) { - case 0: - _make_token(TK_EOF); - break; - case '\\': - INCPOS(1); - if (GETCHAR(0) == '\r') { - INCPOS(1); - } - - if (GETCHAR(0) != '\n') { - _make_error("Expected newline after '\\'."); - return; - } - - INCPOS(1); - line++; - - while (GETCHAR(0) == ' ' || GETCHAR(0) == '\t') { - INCPOS(1); - } - - continue; - case '\t': - case '\r': - case ' ': - INCPOS(1); - continue; - case '#': { // line comment skip -#ifdef DEBUG_ENABLED - String comment; -#endif // DEBUG_ENABLED - while (GETCHAR(0) != '\n') { -#ifdef DEBUG_ENABLED - comment += GETCHAR(0); -#endif // DEBUG_ENABLED - code_pos++; - if (GETCHAR(0) == 0) { //end of file - //_make_error("Unterminated Comment"); - _make_token(TK_EOF); - return; - } - } -#ifdef DEBUG_ENABLED - String comment_content = comment.trim_prefix("#").trim_prefix(" "); - if (comment_content.begins_with("warning-ignore:")) { - String code = comment_content.get_slice(":", 1); - warning_skips.push_back(Pair<int, String>(line, code.strip_edges().to_lower())); - } else if (comment_content.begins_with("warning-ignore-all:")) { - String code = comment_content.get_slice(":", 1); - warning_global_skips.insert(code.strip_edges().to_lower()); - } else if (comment_content.strip_edges() == "warnings-disable") { - ignore_warnings = true; - } -#endif // DEBUG_ENABLED - [[fallthrough]]; - } - case '\n': { - line++; - INCPOS(1); - bool used_spaces = false; - int tabs = 0; - column = 1; - int i = 0; - while (true) { - if (GETCHAR(i) == ' ') { - i++; - used_spaces = true; - } else if (GETCHAR(i) == '\t') { - if (used_spaces) { - _make_error("Spaces used before tabs on a line"); - return; - } - i++; - tabs++; - } else { - break; // not indentation anymore - } - } - - _make_newline(i, tabs); - return; - } - case '/': { - switch (GETCHAR(1)) { - case '=': { // diveq - - _make_token(TK_OP_ASSIGN_DIV); - INCPOS(1); - - } break; - default: - _make_token(TK_OP_DIV); - } - } break; - case '=': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_EQUAL); - INCPOS(1); - - } else { - _make_token(TK_OP_ASSIGN); - } - - } break; - case '<': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_LESS_EQUAL); - INCPOS(1); - } else if (GETCHAR(1) == '<') { - if (GETCHAR(2) == '=') { - _make_token(TK_OP_ASSIGN_SHIFT_LEFT); - INCPOS(1); - } else { - _make_token(TK_OP_SHIFT_LEFT); - } - INCPOS(1); - } else { - _make_token(TK_OP_LESS); - } - - } break; - case '>': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_GREATER_EQUAL); - INCPOS(1); - } else if (GETCHAR(1) == '>') { - if (GETCHAR(2) == '=') { - _make_token(TK_OP_ASSIGN_SHIFT_RIGHT); - INCPOS(1); - - } else { - _make_token(TK_OP_SHIFT_RIGHT); - } - INCPOS(1); - } else { - _make_token(TK_OP_GREATER); - } +static bool _is_alphanumeric(CharType c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} - } break; - case '!': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_NOT_EQUAL); - INCPOS(1); - } else { - _make_token(TK_OP_NOT); - } +static bool _is_digit(CharType c) { + return (c >= '0' && c <= '9'); +} - } break; - //case '"' //string - no strings in shader - //case '\'' //string - no strings in shader - case '{': - _make_token(TK_CURLY_BRACKET_OPEN); - break; - case '}': - _make_token(TK_CURLY_BRACKET_CLOSE); - break; - case '[': - _make_token(TK_BRACKET_OPEN); - break; - case ']': - _make_token(TK_BRACKET_CLOSE); - break; - case '(': - _make_token(TK_PARENTHESIS_OPEN); - break; - case ')': - _make_token(TK_PARENTHESIS_CLOSE); - break; - case ',': - _make_token(TK_COMMA); - break; - case ';': - _make_token(TK_SEMICOLON); - break; - case '?': - _make_token(TK_QUESTION_MARK); - break; - case ':': - _make_token(TK_COLON); //for methods maybe but now useless. - break; - case '$': - _make_token(TK_DOLLAR); //for the get_node() shortener - break; - case '^': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_BIT_XOR); - INCPOS(1); - } else { - _make_token(TK_OP_BIT_XOR); - } +static bool _is_hex_digit(CharType c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} - } break; - case '~': - _make_token(TK_OP_BIT_INVERT); - break; - case '&': { - if (GETCHAR(1) == '&') { - _make_token(TK_OP_AND); - INCPOS(1); - } else if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_BIT_AND); - INCPOS(1); - } else { - _make_token(TK_OP_BIT_AND); - } - } break; - case '|': { - if (GETCHAR(1) == '|') { - _make_token(TK_OP_OR); - INCPOS(1); - } else if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_BIT_OR); - INCPOS(1); - } else { - _make_token(TK_OP_BIT_OR); - } - } break; - case '*': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_MUL); - INCPOS(1); - } else { - _make_token(TK_OP_MUL); - } - } break; - case '+': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_ADD); - INCPOS(1); - /* - } else if (GETCHAR(1)=='+') { - _make_token(TK_OP_PLUS_PLUS); - INCPOS(1); - */ - } else { - _make_token(TK_OP_ADD); - } +static bool _is_binary_digit(CharType c) { + return (c == '0' || c == '1'); +} - } break; - case '-': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_SUB); - INCPOS(1); - } else if (GETCHAR(1) == '>') { - _make_token(TK_FORWARD_ARROW); - INCPOS(1); +GDScriptTokenizer::Token GDScriptTokenizer::make_token(Token::Type p_type) { + Token token(p_type); + token.start_line = start_line; + token.end_line = line; + token.start_column = start_column; + token.end_column = column; + token.leftmost_column = leftmost_column; + token.rightmost_column = rightmost_column; + token.source = String(_start, _current - _start); + + if (p_type != Token::ERROR && cursor_line > -1) { + // Also count whitespace after token. + int offset = 0; + while (_peek(offset) == ' ' || _peek(offset) == '\t') { + offset++; + } + int last_column = column + offset; + // Check cursor position in token. + if (start_line == line) { + // Single line token. + if (cursor_line == start_line && cursor_column >= start_column && cursor_column <= last_column) { + token.cursor_position = cursor_column - start_column; + if (cursor_column == start_column) { + token.cursor_place = CURSOR_BEGINNING; + } else if (cursor_column < column) { + token.cursor_place = CURSOR_MIDDLE; } else { - _make_token(TK_OP_SUB); + token.cursor_place = CURSOR_END; } - } break; - case '%': { - if (GETCHAR(1) == '=') { - _make_token(TK_OP_ASSIGN_MOD); - INCPOS(1); + } + } else { + // Multi line token. + if (cursor_line == start_line && cursor_column >= start_column) { + // Is in first line. + token.cursor_position = cursor_column - start_column; + if (cursor_column == start_column) { + token.cursor_place = CURSOR_BEGINNING; } else { - _make_token(TK_OP_MOD); - } - } break; - case '@': - if (CharType(GETCHAR(1)) != '"' && CharType(GETCHAR(1)) != '\'') { - _make_error("Unexpected '@'"); - return; - } - INCPOS(1); - is_string_name = true; - [[fallthrough]]; - case '\'': - case '"': { - if (GETCHAR(0) == '\'') { - string_mode = STRING_SINGLE_QUOTE; + token.cursor_place = CURSOR_MIDDLE; } - - int i = 1; - if (string_mode == STRING_DOUBLE_QUOTE && GETCHAR(i) == '"' && GETCHAR(i + 1) == '"') { - i += 2; - string_mode = STRING_MULTILINE; - } - - String str; - while (true) { - if (CharType(GETCHAR(i)) == 0) { - _make_error("Unterminated String"); - return; - } else if (string_mode == STRING_DOUBLE_QUOTE && CharType(GETCHAR(i)) == '"') { - break; - } else if (string_mode == STRING_SINGLE_QUOTE && CharType(GETCHAR(i)) == '\'') { - break; - } else if (string_mode == STRING_MULTILINE && CharType(GETCHAR(i)) == '\"' && CharType(GETCHAR(i + 1)) == '\"' && CharType(GETCHAR(i + 2)) == '\"') { - i += 2; - break; - } else if (string_mode != STRING_MULTILINE && CharType(GETCHAR(i)) == '\n') { - _make_error("Unexpected EOL at String."); - return; - } else if (CharType(GETCHAR(i)) == 0xFFFF) { - //string ends here, next will be TK - i--; - break; - } else if (CharType(GETCHAR(i)) == '\\') { - //escaped characters... - i++; - CharType next = GETCHAR(i); - if (next == 0) { - _make_error("Unterminated String"); - return; - } - CharType res = 0; - - switch (next) { - case 'a': - res = '\a'; - break; - case 'b': - res = '\b'; - break; - case 't': - res = '\t'; - break; - case 'n': - res = '\n'; - break; - case 'v': - res = '\v'; - break; - case 'f': - res = '\f'; - break; - case 'r': - res = '\r'; - break; - case '\'': - res = '\''; - break; - case '\"': - res = '\"'; - break; - case '\\': - res = '\\'; - break; - - case 'u': { - // hex number - i += 1; - for (int j = 0; j < 4; j++) { - CharType c = GETCHAR(i + j); - if (c == 0) { - _make_error("Unterminated String"); - return; - } - - CharType v = 0; - if (c >= '0' && c <= '9') { - v = c - '0'; - } else if (c >= 'a' && c <= 'f') { - v = c - 'a'; - v += 10; - } else if (c >= 'A' && c <= 'F') { - v = c - 'A'; - v += 10; - } else { - _make_error("Malformed hex constant in string"); - return; - } - - res <<= 4; - res |= v; - } - i += 3; - - } break; - case '\n': { - line++; - column = 1; - } break; - default: { - _make_error("Invalid escape sequence"); - return; - } break; - } - - if (next != '\n') { - str += res; - } - - } else { - if (CharType(GETCHAR(i)) == '\n') { - line++; - column = 1; - } - - str += CharType(GETCHAR(i)); - } - i++; - } - INCPOS(i); - - if (is_string_name) { - _make_constant(StringName(str)); + } else if (cursor_line == line && cursor_column <= last_column) { + // Is in last line. + token.cursor_position = cursor_column - start_column; + if (cursor_column < column) { + token.cursor_place = CURSOR_MIDDLE; } else { - _make_constant(str); + token.cursor_place = CURSOR_END; } + } else if (cursor_line > start_line && cursor_line < line) { + // Is in middle line. + token.cursor_position = CURSOR_MIDDLE; + } + } + } - } break; - case 0xFFFF: { - _make_token(TK_CURSOR); - } break; - default: { - if (_is_number(GETCHAR(0)) || (GETCHAR(0) == '.' && _is_number(GETCHAR(1)))) { - // parse number - bool period_found = false; - bool exponent_found = false; - bool hexa_found = false; - bool bin_found = false; - bool sign_found = false; - - String str; - int i = 0; - - while (true) { - if (GETCHAR(i) == '.') { - if (period_found || exponent_found) { - _make_error("Invalid numeric constant at '.'"); - return; - } else if (bin_found) { - _make_error("Invalid binary constant at '.'"); - return; - } else if (hexa_found) { - _make_error("Invalid hexadecimal constant at '.'"); - return; - } - period_found = true; - } else if (GETCHAR(i) == 'x') { - if (hexa_found || bin_found || str.length() != 1 || !((i == 1 && str[0] == '0') || (i == 2 && str[1] == '0' && str[0] == '-'))) { - _make_error("Invalid numeric constant at 'x'"); - return; - } - hexa_found = true; - } else if (hexa_found && _is_hex(GETCHAR(i))) { - } else if (!hexa_found && GETCHAR(i) == 'b') { - if (bin_found || str.length() != 1 || !((i == 1 && str[0] == '0') || (i == 2 && str[1] == '0' && str[0] == '-'))) { - _make_error("Invalid numeric constant at 'b'"); - return; - } - bin_found = true; - } else if (!hexa_found && GETCHAR(i) == 'e') { - if (exponent_found || bin_found) { - _make_error("Invalid numeric constant at 'e'"); - return; - } - exponent_found = true; - } else if (_is_number(GETCHAR(i))) { - //all ok - - } else if (bin_found && _is_bin(GETCHAR(i))) { - } else if ((GETCHAR(i) == '-' || GETCHAR(i) == '+') && exponent_found) { - if (sign_found) { - _make_error("Invalid numeric constant at '-'"); - return; - } - sign_found = true; - } else if (GETCHAR(i) == '_') { - i++; - continue; // Included for readability, shouldn't be a part of the string - } else { - break; - } - - str += CharType(GETCHAR(i)); - i++; - } - - if (!(_is_number(str[str.length() - 1]) || (hexa_found && _is_hex(str[str.length() - 1])))) { - _make_error("Invalid numeric constant: " + str); - return; - } - - INCPOS(i); - if (hexa_found) { - int64_t val = str.hex_to_int(); - _make_constant(val); - } else if (bin_found) { - int64_t val = str.bin_to_int(); - _make_constant(val); - } else if (period_found || exponent_found) { - double val = str.to_double(); - _make_constant(val); - } else { - int64_t val = str.to_int(); - _make_constant(val); - } - - return; - } - - if (GETCHAR(0) == '.') { - //parse period - _make_token(TK_PERIOD); - break; - } - - if (_is_text_char(GETCHAR(0))) { - // parse identifier - String str; - str += CharType(GETCHAR(0)); - - int i = 1; - while (_is_text_char(GETCHAR(i))) { - str += CharType(GETCHAR(i)); - i++; - } - - bool identifier = false; - - if (str == "null") { - _make_constant(Variant()); - - } else if (str == "true") { - _make_constant(true); - - } else if (str == "false") { - _make_constant(false); - } else { - bool found = false; - - { - int idx = 0; - - while (_type_list[idx].text) { - if (str == _type_list[idx].text) { - _make_type(_type_list[idx].type); - found = true; - break; - } - idx++; - } - } + return token; +} - if (!found) { - //built in func? +GDScriptTokenizer::Token GDScriptTokenizer::make_literal(const Variant &p_literal) { + Token token = make_token(Token::LITERAL); + token.literal = p_literal; + return token; +} - for (int j = 0; j < GDScriptFunctions::FUNC_MAX; j++) { - if (str == GDScriptFunctions::get_func_name(GDScriptFunctions::Function(j))) { - _make_built_in_func(GDScriptFunctions::Function(j)); - found = true; - break; - } - } - } +GDScriptTokenizer::Token GDScriptTokenizer::make_identifier(const StringName &p_identifier) { + Token identifier = make_token(Token::IDENTIFIER); + identifier.literal = p_identifier; + return identifier; +} - if (!found) { - //keyword +GDScriptTokenizer::Token GDScriptTokenizer::make_error(const String &p_message) { + Token error = make_token(Token::ERROR); + error.literal = p_message; - int idx = 0; - found = false; + return error; +} - while (_keyword_list[idx].text) { - if (str == _keyword_list[idx].text) { - _make_token(_keyword_list[idx].token); - found = true; - break; - } - idx++; - } - } +void GDScriptTokenizer::push_error(const String &p_message) { + Token error = make_error(p_message); + error_stack.push_back(error); +} - if (!found) { - identifier = true; - } - } +void GDScriptTokenizer::push_error(const Token &p_error) { + error_stack.push_back(p_error); +} - if (identifier) { - _make_identifier(str); - } - INCPOS(str.length()); - return; - } +GDScriptTokenizer::Token GDScriptTokenizer::make_paren_error(CharType p_paren) { + if (paren_stack.empty()) { + return make_error(vformat("Closing \"%c\" doesn't have an opening counterpart.", p_paren)); + } + Token error = make_error(vformat("Closing \"%c\" doesn't match the opening \"%c\".", p_paren, paren_stack.back()->get())); + paren_stack.pop_back(); // Remove opening one anyway. + return error; +} - _make_error("Unknown character"); - return; +GDScriptTokenizer::Token GDScriptTokenizer::check_vcs_marker(CharType p_test, Token::Type p_double_type) { + const CharType *next = _current + 1; + int chars = 2; // Two already matched. - } break; + // Test before consuming characters, since we don't want to consume more than needed. + while (*next == p_test) { + chars++; + next++; + } + if (chars >= 7) { + // It is a VCS conflict marker. + while (chars > 1) { + // Consume all characters (first was already consumed by scan()). + _advance(); + chars--; } - - INCPOS(1); - break; + return make_token(Token::VCS_CONFLICT_MARKER); + } else { + // It is only a regular double character token, so we consume the second character. + _advance(); + return make_token(p_double_type); } } -void GDScriptTokenizerText::set_code(const String &p_code) { - code = p_code; - len = p_code.length(); - if (len) { - _code = &code[0]; - } else { - _code = nullptr; +GDScriptTokenizer::Token GDScriptTokenizer::annotation() { + if (!_is_alphanumeric(_peek())) { + push_error("Expected annotation identifier after \"@\"."); } - code_pos = 0; - line = 1; //it is stand-ar-ized that lines begin in 1 in code.. - column = 1; //the same holds for columns - tk_rb_pos = 0; - error_flag = false; -#ifdef DEBUG_ENABLED - ignore_warnings = false; -#endif // DEBUG_ENABLED - last_error = ""; - for (int i = 0; i < MAX_LOOKAHEAD + 1; i++) { + while (_is_alphanumeric(_peek())) { + // Consume all identifier characters. _advance(); } + Token annotation = make_token(Token::ANNOTATION); + annotation.literal = StringName(annotation.source); + return annotation; } -GDScriptTokenizerText::Token GDScriptTokenizerText::get_token(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, TK_ERROR); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, TK_ERROR); - - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - return tk_rb[ofs].type; -} +GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { +#define KEYWORDS(KEYWORD_GROUP, KEYWORD) \ + KEYWORD_GROUP('a') \ + KEYWORD("as", Token::AS) \ + KEYWORD("and", Token::AND) \ + KEYWORD("assert", Token::ASSERT) \ + KEYWORD("await", Token::AWAIT) \ + KEYWORD_GROUP('b') \ + KEYWORD("break", Token::BREAK) \ + KEYWORD("breakpoint", Token::BREAKPOINT) \ + KEYWORD_GROUP('c') \ + KEYWORD("class", Token::CLASS) \ + KEYWORD("class_name", Token::CLASS_NAME) \ + KEYWORD("const", Token::CONST) \ + KEYWORD("continue", Token::CONTINUE) \ + KEYWORD_GROUP('e') \ + KEYWORD("elif", Token::ELIF) \ + KEYWORD("else", Token::ELSE) \ + KEYWORD("enum", Token::ENUM) \ + KEYWORD("extends", Token::EXTENDS) \ + KEYWORD_GROUP('f') \ + KEYWORD("for", Token::FOR) \ + KEYWORD("func", Token::FUNC) \ + KEYWORD_GROUP('i') \ + KEYWORD("if", Token::IF) \ + KEYWORD("in", Token::IN) \ + KEYWORD("is", Token::IS) \ + KEYWORD_GROUP('m') \ + KEYWORD("match", Token::MATCH) \ + KEYWORD_GROUP('n') \ + KEYWORD("namespace", Token::NAMESPACE) \ + KEYWORD("not", Token::NOT) \ + KEYWORD_GROUP('o') \ + KEYWORD("or", Token::OR) \ + KEYWORD_GROUP('p') \ + KEYWORD("pass", Token::PASS) \ + KEYWORD("preload", Token::PRELOAD) \ + KEYWORD_GROUP('r') \ + KEYWORD("return", Token::RETURN) \ + KEYWORD_GROUP('s') \ + KEYWORD("self", Token::SELF) \ + KEYWORD("signal", Token::SIGNAL) \ + KEYWORD("static", Token::STATIC) \ + KEYWORD("super", Token::SUPER) \ + KEYWORD_GROUP('t') \ + KEYWORD("trait", Token::TRAIT) \ + KEYWORD_GROUP('v') \ + KEYWORD("var", Token::VAR) \ + KEYWORD("void", Token::VOID) \ + KEYWORD_GROUP('w') \ + KEYWORD("while", Token::WHILE) \ + KEYWORD_GROUP('y') \ + KEYWORD("yield", Token::YIELD) \ + KEYWORD_GROUP('I') \ + KEYWORD("INF", Token::CONST_INF) \ + KEYWORD_GROUP('N') \ + KEYWORD("NAN", Token::CONST_NAN) \ + KEYWORD_GROUP('P') \ + KEYWORD("PI", Token::CONST_PI) \ + KEYWORD_GROUP('T') \ + KEYWORD("TAU", Token::CONST_TAU) + +#define MIN_KEYWORD_LENGTH 2 +#define MAX_KEYWORD_LENGTH 10 + + // Consume all alphanumeric characters. + while (_is_alphanumeric(_peek())) { + _advance(); + } -int GDScriptTokenizerText::get_token_line(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, -1); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, -1); + int length = _current - _start; - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - return tk_rb[ofs].line; -} + if (length == 1 && _peek(-1) == '_') { + // Lone underscore. + return make_token(Token::UNDERSCORE); + } -int GDScriptTokenizerText::get_token_column(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, -1); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, -1); + String name(_start, length); + if (length < MIN_KEYWORD_LENGTH || length > MAX_KEYWORD_LENGTH) { + // Cannot be a keyword, as the length doesn't match any. + return make_identifier(name); + } - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - return tk_rb[ofs].col; -} + // Define some helper macros for the switch case. +#define KEYWORD_GROUP_CASE(char) \ + break; \ + case char: +#define KEYWORD(keyword, token_type) \ + { \ + const int keyword_length = sizeof(keyword) - 1; \ + static_assert(keyword_length <= MAX_KEYWORD_LENGTH, "There's a keyword longer than the defined maximum length"); \ + static_assert(keyword_length >= MIN_KEYWORD_LENGTH, "There's a keyword shorter than the defined minimum length"); \ + if (keyword_length == length && name == keyword) { \ + return make_token(token_type); \ + } \ + } -const Variant &GDScriptTokenizerText::get_token_constant(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, tk_rb[0].constant); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, tk_rb[0].constant); + // Find if it's a keyword. + switch (_start[0]) { + default: + KEYWORDS(KEYWORD_GROUP_CASE, KEYWORD) + break; + } - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_CONSTANT, tk_rb[0].constant); - return tk_rb[ofs].constant; -} + // Check if it's a special literal + if (length == 4) { + if (name == "true") { + return make_literal(true); + } else if (name == "null") { + return make_literal(Variant()); + } + } else if (length == 5) { + if (name == "false") { + return make_literal(false); + } + } -StringName GDScriptTokenizerText::get_token_identifier(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, StringName()); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, StringName()); + // Not a keyword, so must be an identifier. + return make_identifier(name); - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_IDENTIFIER, StringName()); - return tk_rb[ofs].identifier; +#undef KEYWORDS +#undef MIN_KEYWORD_LENGTH +#undef MAX_KEYWORD_LENGTH +#undef KEYWORD_GROUP_CASE +#undef KEYWORD } -GDScriptFunctions::Function GDScriptTokenizerText::get_token_built_in_func(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, GDScriptFunctions::FUNC_MAX); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, GDScriptFunctions::FUNC_MAX); +void GDScriptTokenizer::newline(bool p_make_token) { + // Don't overwrite previous newline, nor create if we want a line contination. + if (p_make_token && !pending_newline && !line_continuation) { + Token newline(Token::NEWLINE); + newline.start_line = line; + newline.end_line = line; + newline.start_column = column - 1; + newline.end_column = column; + newline.leftmost_column = newline.start_column; + newline.rightmost_column = newline.end_column; + pending_newline = true; + last_newline = newline; + } - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_BUILT_IN_FUNC, GDScriptFunctions::FUNC_MAX); - return tk_rb[ofs].func; + // Increment line/column counters. + line++; + column = 1; + leftmost_column = 1; } -Variant::Type GDScriptTokenizerText::get_token_type(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, Variant::NIL); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, Variant::NIL); +GDScriptTokenizer::Token GDScriptTokenizer::number() { + int base = 10; + bool has_decimal = false; + bool has_exponent = false; + bool has_error = false; + bool (*digit_check_func)(CharType) = _is_digit; + + if (_peek(-1) == '.') { + has_decimal = true; + } else if (_peek(-1) == '0') { + if (_peek() == 'x') { + // Hexadecimal. + base = 16; + digit_check_func = _is_hex_digit; + _advance(); + } else if (_peek() == 'b') { + // Binary. + base = 2; + digit_check_func = _is_binary_digit; + _advance(); + } + } - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_BUILT_IN_TYPE, Variant::NIL); - return tk_rb[ofs].vtype; -} + // Allow '_' to be used in a number, for readability. + while (digit_check_func(_peek()) || _peek() == '_') { + _advance(); + } -int GDScriptTokenizerText::get_token_line_indent(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, 0); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, 0); + // It might be a ".." token (instead of decimal point) so we check if it's not. + if (_peek() == '.' && _peek(1) != '.') { + if (base == 10 && !has_decimal) { + has_decimal = true; + } else if (base == 10) { + Token error = make_error("Cannot use a decimal point twice in a number."); + error.start_column = column; + error.leftmost_column = column; + error.end_column = column + 1; + error.rightmost_column = column + 1; + push_error(error); + has_error = true; + } else if (base == 16) { + Token error = make_error("Cannot use a decimal point in a hexadecimal number."); + error.start_column = column; + error.leftmost_column = column; + error.end_column = column + 1; + error.rightmost_column = column + 1; + push_error(error); + has_error = true; + } else { + Token error = make_error("Cannot use a decimal point in a binary number."); + error.start_column = column; + error.leftmost_column = column; + error.end_column = column + 1; + error.rightmost_column = column + 1; + push_error(error); + has_error = true; + } + if (!has_error) { + _advance(); - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_NEWLINE, 0); - return tk_rb[ofs].constant.operator Vector2().x; -} + // Consume decimal digits. + while (_is_digit(_peek()) || _peek() == '_') { + _advance(); + } + } + } + if (base == 10) { + if (_peek() == 'e' || _peek() == 'E') { + has_exponent = true; + _advance(); + if (_peek() == '+' || _peek() == '-') { + // Exponent sign. + _advance(); + } + // Consume exponent digits. + while (_is_digit(_peek()) || _peek() == '_') { + _advance(); + } + } + } -int GDScriptTokenizerText::get_token_line_tab_indent(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, 0); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, 0); + // Detect extra decimal point. + if (!has_error && has_decimal && _peek() == '.' && _peek(1) != '.') { + Token error = make_error("Cannot use a decimal point twice in a number."); + error.start_column = column; + error.leftmost_column = column; + error.end_column = column + 1; + error.rightmost_column = column + 1; + push_error(error); + has_error = true; + } else if (_is_alphanumeric(_peek())) { + // Letter at the end of the number. + push_error("Invalid numeric notation."); + } - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_NEWLINE, 0); - return tk_rb[ofs].constant.operator Vector2().y; + // Create a string with the whole number. + int length = _current - _start; + String number = String(_start, length).replace("_", ""); + + // Convert to the appropriate literal type. + if (base == 16) { + int64_t value = number.hex_to_int(); + return make_literal(value); + } else if (base == 2) { + int64_t value = number.bin_to_int(); + return make_literal(value); + } else if (has_decimal || has_exponent) { + double value = number.to_float(); + return make_literal(value); + } else { + int64_t value = number.to_int(); + return make_literal(value); + } } -String GDScriptTokenizerText::get_token_error(int p_offset) const { - ERR_FAIL_COND_V(p_offset <= -MAX_LOOKAHEAD, String()); - ERR_FAIL_COND_V(p_offset >= MAX_LOOKAHEAD, String()); +GDScriptTokenizer::Token GDScriptTokenizer::string() { + enum StringType { + STRING_REGULAR, + STRING_NAME, + STRING_NODEPATH, + }; - int ofs = (TK_RB_SIZE + tk_rb_pos + p_offset - MAX_LOOKAHEAD - 1) % TK_RB_SIZE; - ERR_FAIL_COND_V(tk_rb[ofs].type != TK_ERROR, String()); - return tk_rb[ofs].constant; -} + bool is_multiline = false; + StringType type = STRING_REGULAR; -void GDScriptTokenizerText::advance(int p_amount) { - ERR_FAIL_COND(p_amount <= 0); - for (int i = 0; i < p_amount; i++) { + if (_peek(-1) == '&') { + type = STRING_NAME; + _advance(); + } else if (_peek(-1) == '^') { + type = STRING_NODEPATH; _advance(); } -} - -////////////////////////////////////////////////////////////////////////////////////////////////////// -#define BYTECODE_VERSION 13 + CharType quote_char = _peek(-1); -Error GDScriptTokenizerBuffer::set_code_buffer(const Vector<uint8_t> &p_buffer) { - const uint8_t *buf = p_buffer.ptr(); - int total_len = p_buffer.size(); - ERR_FAIL_COND_V(p_buffer.size() < 24 || p_buffer[0] != 'G' || p_buffer[1] != 'D' || p_buffer[2] != 'S' || p_buffer[3] != 'C', ERR_INVALID_DATA); - - int version = decode_uint32(&buf[4]); - ERR_FAIL_COND_V_MSG(version > BYTECODE_VERSION, ERR_INVALID_DATA, "Bytecode is too recent! Please use a newer engine version."); - - int identifier_count = decode_uint32(&buf[8]); - int constant_count = decode_uint32(&buf[12]); - int line_count = decode_uint32(&buf[16]); - int token_count = decode_uint32(&buf[20]); + if (_peek() == quote_char && _peek(1) == quote_char) { + is_multiline = true; + // Consume all quotes. + _advance(); + _advance(); + } - const uint8_t *b = &buf[24]; - total_len -= 24; + String result; - identifiers.resize(identifier_count); - for (int i = 0; i < identifier_count; i++) { - int len = decode_uint32(b); - ERR_FAIL_COND_V(len > total_len, ERR_INVALID_DATA); - b += 4; - Vector<uint8_t> cs; - cs.resize(len); - for (int j = 0; j < len; j++) { - cs.write[j] = b[j] ^ 0xb6; + for (;;) { + // Consume actual string. + if (_is_at_end()) { + return make_error("Unterminated string."); } - cs.write[cs.size() - 1] = 0; - String s; - s.parse_utf8((const char *)cs.ptr()); - b += len; - total_len -= len + 4; - identifiers.write[i] = s; - } + CharType ch = _peek(); - constants.resize(constant_count); - for (int i = 0; i < constant_count; i++) { - Variant v; - int len; - // An object cannot be constant, never decode objects - Error err = decode_variant(v, b, total_len, &len, false); - if (err) { - return err; - } - b += len; - total_len -= len; - constants.write[i] = v; - } + if (ch == '\\') { + // Escape pattern. + _advance(); + if (_is_at_end()) { + return make_error("Unterminated string."); + } - ERR_FAIL_COND_V(line_count * 8 > total_len, ERR_INVALID_DATA); + // Grab escape character. + CharType code = _peek(); + _advance(); + if (_is_at_end()) { + return make_error("Unterminated string."); + } - for (int i = 0; i < line_count; i++) { - uint32_t token = decode_uint32(b); - b += 4; - uint32_t linecol = decode_uint32(b); - b += 4; + CharType escaped = 0; + bool valid_escape = true; - lines.insert(token, linecol); - total_len -= 8; - } + switch (code) { + case 'a': + escaped = '\a'; + break; + case 'b': + escaped = '\b'; + break; + case 'f': + escaped = '\f'; + break; + case 'n': + escaped = '\n'; + break; + case 'r': + escaped = '\r'; + break; + case 't': + escaped = '\t'; + break; + case 'v': + escaped = '\v'; + break; + case '\'': + escaped = '\''; + break; + case '\"': + escaped = '\"'; + break; + case '\\': + escaped = '\\'; + break; + case 'u': + // Hexadecimal sequence. + for (int i = 0; i < 4; i++) { + if (_is_at_end()) { + return make_error("Unterminated string."); + } - tokens.resize(token_count); + CharType digit = _peek(); + CharType value = 0; + if (digit >= '0' && digit <= '9') { + value = digit - '0'; + } else if (digit >= 'a' && digit <= 'f') { + value = digit - 'a'; + value += 10; + } else if (digit >= 'A' && digit <= 'F') { + value = digit - 'A'; + value += 10; + } else { + // Make error, but keep parsing the string. + Token error = make_error("Invalid hexadecimal digit in unicode escape sequence."); + error.start_column = column; + error.leftmost_column = error.start_column; + error.end_column = column + 1; + error.rightmost_column = error.end_column; + push_error(error); + valid_escape = false; + break; + } - for (int i = 0; i < token_count; i++) { - ERR_FAIL_COND_V(total_len < 1, ERR_INVALID_DATA); + escaped <<= 4; + escaped |= value; - if ((*b) & TOKEN_BYTE_MASK) { //little endian always - ERR_FAIL_COND_V(total_len < 4, ERR_INVALID_DATA); + _advance(); + } + break; + case '\r': + if (_peek() != '\n') { + // Carriage return without newline in string. (???) + // Just add it to the string and keep going. + result += ch; + _advance(); + break; + } + [[fallthrough]]; + case '\n': + // Escaping newline. + newline(false); + valid_escape = false; // Don't add to the string. + break; + default: + Token error = make_error("Invalid escape in string."); + error.start_column = column - 2; + error.leftmost_column = error.start_column; + push_error(error); + valid_escape = false; + break; + } - tokens.write[i] = decode_uint32(b) & ~TOKEN_BYTE_MASK; - b += 4; + if (valid_escape) { + result += escaped; + } + } else if (ch == quote_char) { + _advance(); + if (is_multiline) { + if (_peek() == quote_char && _peek(1) == quote_char) { + // Ended the multiline string. Consume all quotes. + _advance(); + _advance(); + break; + } + } else { + // Ended single-line string. + break; + } } else { - tokens.write[i] = *b; - b += 1; - total_len--; + result += ch; + _advance(); + if (ch == '\n') { + newline(false); + } } } - token = 0; + // Make the literal. + Variant string; + switch (type) { + case STRING_NAME: + string = StringName(result); + break; + case STRING_NODEPATH: + string = NodePath(result); + break; + case STRING_REGULAR: + string = result; + break; + } - return OK; + return make_literal(string); } -Vector<uint8_t> GDScriptTokenizerBuffer::parse_code_string(const String &p_code) { - Vector<uint8_t> buf; +void GDScriptTokenizer::check_indent() { + ERR_FAIL_COND_MSG(column != 1, "Checking tokenizer indentation in the middle of a line."); - Map<StringName, int> identifier_map; - HashMap<Variant, int, VariantHasher, VariantComparator> constant_map; - Map<uint32_t, int> line_map; - Vector<uint32_t> token_array; + if (_is_at_end()) { + // Send dedents for every indent level. + pending_indents -= indent_level(); + indent_stack.clear(); + return; + } - GDScriptTokenizerText tt; - tt.set_code(p_code); - int line = -1; + for (;;) { + CharType current_indent_char = _peek(); + int indent_count = 0; - while (true) { - if (tt.get_token_line() != line) { - line = tt.get_token_line(); - line_map[line] = token_array.size(); + if (current_indent_char != ' ' && current_indent_char != '\t' && current_indent_char != '\r' && current_indent_char != '\n' && current_indent_char != '#') { + // First character of the line is not whitespace, so we clear all indentation levels. + // Unless we are in a continuation or in multiline mode (inside expression). + if (line_continuation || multiline_mode) { + return; + } + pending_indents -= indent_level(); + indent_stack.clear(); + return; } - uint32_t token = tt.get_token(); - switch (tt.get_token()) { - case TK_IDENTIFIER: { - StringName id = tt.get_token_identifier(); - if (!identifier_map.has(id)) { - int idx = identifier_map.size(); - identifier_map[id] = idx; - } - token |= identifier_map[id] << TOKEN_BITS; - } break; - case TK_CONSTANT: { - const Variant &c = tt.get_token_constant(); - if (!constant_map.has(c)) { - int idx = constant_map.size(); - constant_map[c] = idx; - } - token |= constant_map[c] << TOKEN_BITS; - } break; - case TK_BUILT_IN_TYPE: { - token |= tt.get_token_type() << TOKEN_BITS; - } break; - case TK_BUILT_IN_FUNC: { - token |= tt.get_token_built_in_func() << TOKEN_BITS; - - } break; - case TK_NEWLINE: { - token |= tt.get_token_line_indent() << TOKEN_BITS; - } break; - case TK_ERROR: { - ERR_FAIL_V(Vector<uint8_t>()); - } break; - default: { + if (_peek() == '\r') { + _advance(); + if (_peek() != '\n') { + push_error("Stray carriage return character in source code."); } - }; - - token_array.push_back(token); - - if (tt.get_token() == TK_EOF) { - break; } - tt.advance(); - } - - //reverse maps - - Map<int, StringName> rev_identifier_map; - for (Map<StringName, int>::Element *E = identifier_map.front(); E; E = E->next()) { - rev_identifier_map[E->get()] = E->key(); - } + if (_peek() == '\n') { + // Empty line, keep going. + _advance(); + newline(false); + continue; + } - Map<int, Variant> rev_constant_map; - const Variant *K = nullptr; - while ((K = constant_map.next(K))) { - rev_constant_map[constant_map[*K]] = *K; - } + // Check indent level. + bool mixed = false; + while (!_is_at_end()) { + CharType space = _peek(); + if (space == '\t') { + // Consider individual tab columns. + column += tab_size - 1; + indent_count += tab_size; + } else if (space == ' ') { + indent_count += 1; + } else { + break; + } + mixed = mixed || space != current_indent_char; + _advance(); + } - Map<int, uint32_t> rev_line_map; - for (Map<uint32_t, int>::Element *E = line_map.front(); E; E = E->next()) { - rev_line_map[E->get()] = E->key(); - } + if (mixed) { + Token error = make_error("Mixed use of tabs and spaces for indentation."); + error.start_line = line; + error.start_column = 1; + error.leftmost_column = 1; + error.rightmost_column = column; + push_error(error); + } - //save header - buf.resize(24); - buf.write[0] = 'G'; - buf.write[1] = 'D'; - buf.write[2] = 'S'; - buf.write[3] = 'C'; - encode_uint32(BYTECODE_VERSION, &buf.write[4]); - encode_uint32(identifier_map.size(), &buf.write[8]); - encode_uint32(constant_map.size(), &buf.write[12]); - encode_uint32(line_map.size(), &buf.write[16]); - encode_uint32(token_array.size(), &buf.write[20]); - - //save identifiers - - for (Map<int, StringName>::Element *E = rev_identifier_map.front(); E; E = E->next()) { - CharString cs = String(E->get()).utf8(); - int len = cs.length() + 1; - int extra = 4 - (len % 4); - if (extra == 4) { - extra = 0; + if (_is_at_end()) { + // Reached the end with an empty line, so just dedent as much as needed. + pending_indents -= indent_level(); + indent_stack.clear(); + return; } - uint8_t ibuf[4]; - encode_uint32(len + extra, ibuf); - for (int i = 0; i < 4; i++) { - buf.push_back(ibuf[i]); + if (_peek() == '\r') { + _advance(); + if (_peek() != '\n') { + push_error("Stray carriage return character in source code."); + } } - for (int i = 0; i < len; i++) { - buf.push_back(cs[i] ^ 0xb6); + if (_peek() == '\n') { + // Empty line, keep going. + _advance(); + newline(false); + continue; } - for (int i = 0; i < extra; i++) { - buf.push_back(0 ^ 0xb6); + if (_peek() == '#') { + // Comment. Advance to the next line. + while (_peek() != '\n' && !_is_at_end()) { + _advance(); + } + if (_is_at_end()) { + // Reached the end with an empty line, so just dedent as much as needed. + pending_indents -= indent_level(); + indent_stack.clear(); + return; + } + _advance(); // Consume '\n'. + newline(false); + continue; } - } - for (Map<int, Variant>::Element *E = rev_constant_map.front(); E; E = E->next()) { - int len; - // Objects cannot be constant, never encode objects - Error err = encode_variant(E->get(), nullptr, len, false); - ERR_FAIL_COND_V_MSG(err != OK, Vector<uint8_t>(), "Error when trying to encode Variant."); - int pos = buf.size(); - buf.resize(pos + len); - encode_variant(E->get(), &buf.write[pos], len, false); - } + if (line_continuation || multiline_mode) { + // We cleared up all the whitespace at the beginning of the line. + // But if this is a continuation or multiline mode and we don't want any indentation change. + return; + } - for (Map<int, uint32_t>::Element *E = rev_line_map.front(); E; E = E->next()) { - uint8_t ibuf[8]; - encode_uint32(E->key(), &ibuf[0]); - encode_uint32(E->get(), &ibuf[4]); - for (int i = 0; i < 8; i++) { - buf.push_back(ibuf[i]); + // Check if indentation character is consistent. + if (indent_char == '\0') { + // First time indenting, choose character now. + indent_char = current_indent_char; + } else if (current_indent_char != indent_char) { + Token error = make_error(vformat("Used \"%c\" for indentation instead \"%c\" as used before in the file.", String(¤t_indent_char, 1).c_escape(), String(&indent_char, 1).c_escape())); + error.start_line = line; + error.start_column = 1; + error.leftmost_column = 1; + error.rightmost_column = column; + push_error(error); } - } - for (int i = 0; i < token_array.size(); i++) { - uint32_t token = token_array[i]; + // Now we can do actual indentation changes. - if (token & ~TOKEN_MASK) { - uint8_t buf4[4]; - encode_uint32(token_array[i] | TOKEN_BYTE_MASK, &buf4[0]); - for (int j = 0; j < 4; j++) { - buf.push_back(buf4[j]); - } + // Check if indent or dedent. + int previous_indent = 0; + if (indent_level() > 0) { + previous_indent = indent_stack.back()->get(); + } + if (indent_count == previous_indent) { + // No change in indentation. + return; + } + if (indent_count > previous_indent) { + // Indentation increased. + indent_stack.push_back(indent_count); + pending_indents++; } else { - buf.push_back(token); + // Indentation decreased (dedent). + if (indent_level() == 0) { + push_error("Tokenizer bug: trying to dedent without previous indent."); + return; + } + while (indent_level() > 0 && indent_stack.back()->get() > indent_count) { + indent_stack.pop_back(); + pending_indents--; + } + if ((indent_level() > 0 && indent_stack.back()->get() != indent_count) || (indent_level() == 0 && indent_count != 0)) { + // Mismatched indentation alignment. + Token error = make_error("Unindent doesn't match the previous indentation level."); + error.start_line = line; + error.start_column = 1; + error.leftmost_column = 1; + error.end_column = column + 1; + error.rightmost_column = column + 1; + push_error(error); + // Still, we'll be lenient and keep going, so keep this level in the stack. + indent_stack.push_back(indent_count); + } } + break; // Get out of the loop in any case. } - - return buf; } -GDScriptTokenizerBuffer::Token GDScriptTokenizerBuffer::get_token(int p_offset) const { - int offset = token + p_offset; - - if (offset < 0 || offset >= tokens.size()) { - return TK_EOF; +void GDScriptTokenizer::_skip_whitespace() { + if (pending_indents != 0) { + // Still have some indent/dedent tokens to give. + return; } - return GDScriptTokenizerBuffer::Token(tokens[offset] & TOKEN_MASK); -} - -StringName GDScriptTokenizerBuffer::get_token_identifier(int p_offset) const { - int offset = token + p_offset; + bool is_bol = column == 1; // Beginning of line. - ERR_FAIL_INDEX_V(offset, tokens.size(), StringName()); - uint32_t identifier = tokens[offset] >> TOKEN_BITS; - ERR_FAIL_UNSIGNED_INDEX_V(identifier, (uint32_t)identifiers.size(), StringName()); + if (is_bol) { + check_indent(); + return; + } - return identifiers[identifier]; + for (;;) { + CharType c = _peek(); + switch (c) { + case ' ': + _advance(); + break; + case '\t': + _advance(); + // Consider individual tab columns. + column += tab_size - 1; + break; + case '\r': + _advance(); // Consume either way. + if (_peek() != '\n') { + push_error("Stray carriage return character in source code."); + return; + } + break; + case '\n': + _advance(); + newline(!is_bol); // Don't create new line token if line is empty. + check_indent(); + break; + case '#': + // Comment. + while (_peek() != '\n' && !_is_at_end()) { + _advance(); + } + if (_is_at_end()) { + return; + } + _advance(); // Consume '\n' + newline(!is_bol); + check_indent(); + break; + default: + return; + } + } } -GDScriptFunctions::Function GDScriptTokenizerBuffer::get_token_built_in_func(int p_offset) const { - int offset = token + p_offset; - ERR_FAIL_INDEX_V(offset, tokens.size(), GDScriptFunctions::FUNC_MAX); - return GDScriptFunctions::Function(tokens[offset] >> TOKEN_BITS); -} +GDScriptTokenizer::Token GDScriptTokenizer::scan() { + if (has_error()) { + return pop_error(); + } -Variant::Type GDScriptTokenizerBuffer::get_token_type(int p_offset) const { - int offset = token + p_offset; - ERR_FAIL_INDEX_V(offset, tokens.size(), Variant::NIL); + _skip_whitespace(); - return Variant::Type(tokens[offset] >> TOKEN_BITS); -} + if (pending_newline) { + pending_newline = false; + if (!multiline_mode) { + // Don't return newline tokens on multine mode. + return last_newline; + } + } -int GDScriptTokenizerBuffer::get_token_line(int p_offset) const { - int offset = token + p_offset; - int pos = lines.find_nearest(offset); + // Check for potential errors after skipping whitespace(). + if (has_error()) { + return pop_error(); + } - if (pos < 0) { - return -1; + _start = _current; + start_line = line; + start_column = column; + leftmost_column = column; + rightmost_column = column; + + if (pending_indents != 0) { + // Adjust position for indent. + _start -= start_column - 1; + start_column = 1; + leftmost_column = 1; + if (pending_indents > 0) { + // Indents. + pending_indents--; + return make_token(Token::INDENT); + } else { + // Dedents. + pending_indents++; + Token dedent = make_token(Token::DEDENT); + dedent.end_column += 1; + dedent.rightmost_column += 1; + return dedent; + } } - if (pos >= lines.size()) { - pos = lines.size() - 1; + + if (_is_at_end()) { + return make_token(Token::TK_EOF); } - uint32_t l = lines.getv(pos); - return l & TOKEN_LINE_MASK; -} + const CharType c = _advance(); -int GDScriptTokenizerBuffer::get_token_column(int p_offset) const { - int offset = token + p_offset; - int pos = lines.find_nearest(offset); - if (pos < 0) { - return -1; - } - if (pos >= lines.size()) { - pos = lines.size() - 1; + if (c == '\\') { + // Line continuation with backslash. + if (_peek() == '\r') { + if (_peek(1) != '\n') { + return make_error("Unexpected carriage return character."); + } + _advance(); + } + if (_peek() != '\n') { + return make_error("Expected new line after \"\\\"."); + } + _advance(); + newline(false); + line_continuation = true; + return scan(); // Recurse to get next token. } - uint32_t l = lines.getv(pos); - return l >> TOKEN_LINE_BITS; -} + line_continuation = false; -int GDScriptTokenizerBuffer::get_token_line_indent(int p_offset) const { - int offset = token + p_offset; - ERR_FAIL_INDEX_V(offset, tokens.size(), 0); - return tokens[offset] >> TOKEN_BITS; -} + if (_is_digit(c)) { + return number(); + } else if (_is_alphanumeric(c)) { + return potential_identifier(); + } -const Variant &GDScriptTokenizerBuffer::get_token_constant(int p_offset) const { - int offset = token + p_offset; - ERR_FAIL_INDEX_V(offset, tokens.size(), nil); - uint32_t constant = tokens[offset] >> TOKEN_BITS; - ERR_FAIL_UNSIGNED_INDEX_V(constant, (uint32_t)constants.size(), nil); - return constants[constant]; -} + switch (c) { + // String literals. + case '"': + case '\'': + return string(); + + // Annotation. + case '@': + return annotation(); + + // Single characters. + case '~': + return make_token(Token::TILDE); + case ',': + return make_token(Token::COMMA); + case ':': + return make_token(Token::COLON); + case ';': + return make_token(Token::SEMICOLON); + case '$': + return make_token(Token::DOLLAR); + case '?': + return make_token(Token::QUESTION_MARK); + case '`': + return make_token(Token::BACKTICK); + + // Parens. + case '(': + push_paren('('); + return make_token(Token::PARENTHESIS_OPEN); + case '[': + push_paren('['); + return make_token(Token::BRACKET_OPEN); + case '{': + push_paren('{'); + return make_token(Token::BRACE_OPEN); + case ')': + if (!pop_paren('(')) { + return make_paren_error(c); + } + return make_token(Token::PARENTHESIS_CLOSE); + case ']': + if (!pop_paren('[')) { + return make_paren_error(c); + } + return make_token(Token::BRACKET_CLOSE); + case '}': + if (!pop_paren('{')) { + return make_paren_error(c); + } + return make_token(Token::BRACE_CLOSE); + + // Double characters. + case '!': + if (_peek() == '=') { + _advance(); + return make_token(Token::BANG_EQUAL); + } else { + return make_token(Token::BANG); + } + case '.': + if (_peek() == '.') { + _advance(); + return make_token(Token::PERIOD_PERIOD); + } else if (_is_digit(_peek())) { + // Number starting with '.'. + return number(); + } else { + return make_token(Token::PERIOD); + } + case '+': + if (_peek() == '=') { + _advance(); + return make_token(Token::PLUS_EQUAL); + } else { + return make_token(Token::PLUS); + } + case '-': + if (_peek() == '=') { + _advance(); + return make_token(Token::MINUS_EQUAL); + } else if (_peek() == '>') { + _advance(); + return make_token(Token::FORWARD_ARROW); + } else { + return make_token(Token::MINUS); + } + case '*': + if (_peek() == '=') { + _advance(); + return make_token(Token::STAR_EQUAL); + } else { + return make_token(Token::STAR); + } + case '/': + if (_peek() == '=') { + _advance(); + return make_token(Token::SLASH_EQUAL); + } else { + return make_token(Token::SLASH); + } + case '%': + if (_peek() == '=') { + _advance(); + return make_token(Token::PERCENT_EQUAL); + } else { + return make_token(Token::PERCENT); + } + case '^': + if (_peek() == '=') { + _advance(); + return make_token(Token::CARET_EQUAL); + } else if (_peek() == '"' || _peek() == '\'') { + // Node path + return string(); + } else { + return make_token(Token::CARET); + } + case '&': + if (_peek() == '&') { + _advance(); + return make_token(Token::AMPERSAND_AMPERSAND); + } else if (_peek() == '=') { + _advance(); + return make_token(Token::AMPERSAND_EQUAL); + } else if (_peek() == '"' || _peek() == '\'') { + // String Name + return string(); + } else { + return make_token(Token::AMPERSAND); + } + case '|': + if (_peek() == '|') { + _advance(); + return make_token(Token::PIPE_PIPE); + } else if (_peek() == '=') { + _advance(); + return make_token(Token::PIPE_EQUAL); + } else { + return make_token(Token::PIPE); + } -String GDScriptTokenizerBuffer::get_token_error(int p_offset) const { - ERR_FAIL_V(String()); -} + // Potential VCS conflict markers. + case '=': + if (_peek() == '=') { + return check_vcs_marker('=', Token::EQUAL_EQUAL); + } else { + return make_token(Token::EQUAL); + } + case '<': + if (_peek() == '=') { + _advance(); + return make_token(Token::LESS_EQUAL); + } else if (_peek() == '<') { + if (_peek(1) == '=') { + _advance(); + _advance(); // Advance both '<' and '=' + return make_token(Token::LESS_LESS_EQUAL); + } else { + return check_vcs_marker('<', Token::LESS_LESS); + } + } else { + return make_token(Token::LESS); + } + case '>': + if (_peek() == '=') { + _advance(); + return make_token(Token::GREATER_EQUAL); + } else if (_peek() == '>') { + if (_peek(1) == '=') { + _advance(); + _advance(); // Advance both '>' and '=' + return make_token(Token::GREATER_GREATER_EQUAL); + } else { + return check_vcs_marker('>', Token::GREATER_GREATER); + } + } else { + return make_token(Token::GREATER); + } -void GDScriptTokenizerBuffer::advance(int p_amount) { - ERR_FAIL_INDEX(p_amount + token, tokens.size()); - token += p_amount; + default: + return make_error(vformat(R"(Unknown character "%s".")", String(&c, 1))); + } } -GDScriptTokenizerBuffer::GDScriptTokenizerBuffer() { - token = 0; +GDScriptTokenizer::GDScriptTokenizer() { +#ifdef TOOLS_ENABLED + if (EditorSettings::get_singleton()) { + tab_size = EditorSettings::get_singleton()->get_setting("text_editor/indent/size"); + } +#endif // TOOLS_ENABLED } diff --git a/modules/gdscript/gdscript_tokenizer.h b/modules/gdscript/gdscript_tokenizer.h index 32603c010f..100ed3f132 100644 --- a/modules/gdscript/gdscript_tokenizer.h +++ b/modules/gdscript/gdscript_tokenizer.h @@ -31,268 +31,221 @@ #ifndef GDSCRIPT_TOKENIZER_H #define GDSCRIPT_TOKENIZER_H -#include "core/pair.h" +#include "core/list.h" #include "core/set.h" -#include "core/string_name.h" -#include "core/ustring.h" #include "core/variant.h" -#include "core/vmap.h" -#include "gdscript_functions.h" +#include "core/vector.h" class GDScriptTokenizer { public: - enum Token { - - TK_EMPTY, - TK_IDENTIFIER, - TK_CONSTANT, - TK_SELF, - TK_BUILT_IN_TYPE, - TK_BUILT_IN_FUNC, - TK_OP_IN, - TK_OP_EQUAL, - TK_OP_NOT_EQUAL, - TK_OP_LESS, - TK_OP_LESS_EQUAL, - TK_OP_GREATER, - TK_OP_GREATER_EQUAL, - TK_OP_AND, - TK_OP_OR, - TK_OP_NOT, - TK_OP_ADD, - TK_OP_SUB, - TK_OP_MUL, - TK_OP_DIV, - TK_OP_MOD, - TK_OP_SHIFT_LEFT, - TK_OP_SHIFT_RIGHT, - TK_OP_ASSIGN, - TK_OP_ASSIGN_ADD, - TK_OP_ASSIGN_SUB, - TK_OP_ASSIGN_MUL, - TK_OP_ASSIGN_DIV, - TK_OP_ASSIGN_MOD, - TK_OP_ASSIGN_SHIFT_LEFT, - TK_OP_ASSIGN_SHIFT_RIGHT, - TK_OP_ASSIGN_BIT_AND, - TK_OP_ASSIGN_BIT_OR, - TK_OP_ASSIGN_BIT_XOR, - TK_OP_BIT_AND, - TK_OP_BIT_OR, - TK_OP_BIT_XOR, - TK_OP_BIT_INVERT, - //TK_OP_PLUS_PLUS, - //TK_OP_MINUS_MINUS, - TK_CF_IF, - TK_CF_ELIF, - TK_CF_ELSE, - TK_CF_FOR, - TK_CF_WHILE, - TK_CF_BREAK, - TK_CF_CONTINUE, - TK_CF_PASS, - TK_CF_RETURN, - TK_CF_MATCH, - TK_PR_FUNCTION, - TK_PR_CLASS, - TK_PR_CLASS_NAME, - TK_PR_EXTENDS, - TK_PR_IS, - TK_PR_ONREADY, - TK_PR_TOOL, - TK_PR_STATIC, - TK_PR_EXPORT, - TK_PR_SETGET, - TK_PR_CONST, - TK_PR_VAR, - TK_PR_AS, - TK_PR_VOID, - TK_PR_ENUM, - TK_PR_PRELOAD, - TK_PR_ASSERT, - TK_PR_YIELD, - TK_PR_SIGNAL, - TK_PR_BREAKPOINT, - TK_PR_REMOTE, - TK_PR_MASTER, - TK_PR_PUPPET, - TK_PR_REMOTESYNC, - TK_PR_MASTERSYNC, - TK_PR_PUPPETSYNC, - TK_BRACKET_OPEN, - TK_BRACKET_CLOSE, - TK_CURLY_BRACKET_OPEN, - TK_CURLY_BRACKET_CLOSE, - TK_PARENTHESIS_OPEN, - TK_PARENTHESIS_CLOSE, - TK_COMMA, - TK_SEMICOLON, - TK_PERIOD, - TK_QUESTION_MARK, - TK_COLON, - TK_DOLLAR, - TK_FORWARD_ARROW, - TK_NEWLINE, - TK_CONST_PI, - TK_CONST_TAU, - TK_WILDCARD, - TK_CONST_INF, - TK_CONST_NAN, - TK_ERROR, - TK_EOF, - TK_CURSOR, //used for code completion - TK_MAX - }; - -protected: - enum StringMode { - STRING_SINGLE_QUOTE, - STRING_DOUBLE_QUOTE, - STRING_MULTILINE + enum CursorPlace { + CURSOR_NONE, + CURSOR_BEGINNING, + CURSOR_MIDDLE, + CURSOR_END, }; - static const char *token_names[TK_MAX]; - -public: - static const char *get_token_name(Token p_token); - - bool is_token_literal(int p_offset = 0, bool variable_safe = false) const; - StringName get_token_literal(int p_offset = 0) const; - - virtual const Variant &get_token_constant(int p_offset = 0) const = 0; - virtual Token get_token(int p_offset = 0) const = 0; - virtual StringName get_token_identifier(int p_offset = 0) const = 0; - virtual GDScriptFunctions::Function get_token_built_in_func(int p_offset = 0) const = 0; - virtual Variant::Type get_token_type(int p_offset = 0) const = 0; - virtual int get_token_line(int p_offset = 0) const = 0; - virtual int get_token_column(int p_offset = 0) const = 0; - virtual int get_token_line_indent(int p_offset = 0) const = 0; - virtual int get_token_line_tab_indent(int p_offset = 0) const = 0; - virtual String get_token_error(int p_offset = 0) const = 0; - virtual void advance(int p_amount = 1) = 0; -#ifdef DEBUG_ENABLED - virtual const Vector<Pair<int, String>> &get_warning_skips() const = 0; - virtual const Set<String> &get_warning_global_skips() const = 0; - virtual bool is_ignoring_warnings() const = 0; -#endif // DEBUG_ENABLED - - virtual ~GDScriptTokenizer() {} -}; - -class GDScriptTokenizerText : public GDScriptTokenizer { - enum { - MAX_LOOKAHEAD = 4, - TK_RB_SIZE = MAX_LOOKAHEAD * 2 + 1 + struct Token { + enum Type { + EMPTY, + // Basic + ANNOTATION, + IDENTIFIER, + LITERAL, + // Comparison + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, + EQUAL_EQUAL, + BANG_EQUAL, + // Logical + AND, + OR, + NOT, + AMPERSAND_AMPERSAND, + PIPE_PIPE, + BANG, + // Bitwise + AMPERSAND, + PIPE, + TILDE, + CARET, + LESS_LESS, + GREATER_GREATER, + // Math + PLUS, + MINUS, + STAR, + SLASH, + PERCENT, + // Assignment + EQUAL, + PLUS_EQUAL, + MINUS_EQUAL, + STAR_EQUAL, + SLASH_EQUAL, + PERCENT_EQUAL, + LESS_LESS_EQUAL, + GREATER_GREATER_EQUAL, + AMPERSAND_EQUAL, + PIPE_EQUAL, + CARET_EQUAL, + // Control flow + IF, + ELIF, + ELSE, + FOR, + WHILE, + BREAK, + CONTINUE, + PASS, + RETURN, + MATCH, + // Keywords + AS, + ASSERT, + AWAIT, + BREAKPOINT, + CLASS, + CLASS_NAME, + CONST, + ENUM, + EXTENDS, + FUNC, + IN, + IS, + NAMESPACE, + PRELOAD, + SELF, + SIGNAL, + STATIC, + SUPER, + TRAIT, + VAR, + VOID, + YIELD, + // Punctuation + BRACKET_OPEN, + BRACKET_CLOSE, + BRACE_OPEN, + BRACE_CLOSE, + PARENTHESIS_OPEN, + PARENTHESIS_CLOSE, + COMMA, + SEMICOLON, + PERIOD, + PERIOD_PERIOD, + COLON, + DOLLAR, + FORWARD_ARROW, + UNDERSCORE, + // Whitespace + NEWLINE, + INDENT, + DEDENT, + // Constants + CONST_PI, + CONST_TAU, + CONST_INF, + CONST_NAN, + // Error message improvement + VCS_CONFLICT_MARKER, + BACKTICK, + QUESTION_MARK, + // Special + ERROR, + TK_EOF, // "EOF" is reserved + TK_MAX + }; - }; + Type type = EMPTY; + Variant literal; + int start_line = 0, end_line = 0, start_column = 0, end_column = 0; + int leftmost_column = 0, rightmost_column = 0; // Column span for multiline tokens. + int cursor_position = -1; + CursorPlace cursor_place = CURSOR_NONE; + String source; + + const char *get_name() const; + bool is_identifier() const; + bool is_node_name() const; + StringName get_identifier() const { return source; } + + Token(Type p_type) { + type = p_type; + } - struct TokenData { - Token type; - StringName identifier; //for identifier types - Variant constant; //for constant types - union { - Variant::Type vtype; //for type types - GDScriptFunctions::Function func; //function for built in functions - int warning_code; //for warning skip - }; - int line, col; - TokenData() { - type = TK_EMPTY; - line = col = 0; - vtype = Variant::NIL; + Token() { + type = EMPTY; } }; - void _make_token(Token p_type); - void _make_newline(int p_indentation = 0, int p_tabs = 0); - void _make_identifier(const StringName &p_identifier); - void _make_built_in_func(GDScriptFunctions::Function p_func); - void _make_constant(const Variant &p_constant); - void _make_type(const Variant::Type &p_type); - void _make_error(const String &p_error); - - String code; - int len; - int code_pos; - const CharType *_code; - int line; - int column; - TokenData tk_rb[TK_RB_SIZE * 2 + 1]; - int tk_rb_pos; - String last_error; - bool error_flag; - -#ifdef DEBUG_ENABLED - Vector<Pair<int, String>> warning_skips; - Set<String> warning_global_skips; - bool ignore_warnings; -#endif // DEBUG_ENABLED - - void _advance(); +private: + String source; + const CharType *_source = nullptr; + const CharType *_current = nullptr; + int line = -1, column = -1; + int cursor_line = -1, cursor_column = -1; + int tab_size = 4; + + // Keep track of multichar tokens. + const CharType *_start = nullptr; + int start_line = 0, start_column = 0; + int leftmost_column = 0, rightmost_column = 0; + + // Info cache. + bool line_continuation = false; // Whether this line is a continuation of the previous, like when using '\'. + bool multiline_mode = false; + List<Token> error_stack; + bool pending_newline = false; + Token last_newline; + int pending_indents = 0; + List<int> indent_stack; + List<CharType> paren_stack; + CharType indent_char = '\0'; + int position = 0; + int length = 0; + + _FORCE_INLINE_ bool _is_at_end() { return position >= length; } + _FORCE_INLINE_ CharType _peek(int p_offset = 0) { return position + p_offset >= 0 && position + p_offset < length ? _current[p_offset] : '\0'; } + int indent_level() const { return indent_stack.size(); } + bool has_error() const { return !error_stack.empty(); } + Token pop_error(); + CharType _advance(); + void _skip_whitespace(); + void check_indent(); + + Token make_error(const String &p_message); + void push_error(const String &p_message); + void push_error(const Token &p_error); + Token make_paren_error(CharType p_paren); + Token make_token(Token::Type p_type); + Token make_literal(const Variant &p_literal); + Token make_identifier(const StringName &p_identifier); + Token check_vcs_marker(CharType p_test, Token::Type p_double_type); + void push_paren(CharType p_char); + bool pop_paren(CharType p_expected); + + void newline(bool p_make_token); + Token number(); + Token potential_identifier(); + Token string(); + Token annotation(); public: - void set_code(const String &p_code); - virtual Token get_token(int p_offset = 0) const; - virtual StringName get_token_identifier(int p_offset = 0) const; - virtual GDScriptFunctions::Function get_token_built_in_func(int p_offset = 0) const; - virtual Variant::Type get_token_type(int p_offset = 0) const; - virtual int get_token_line(int p_offset = 0) const; - virtual int get_token_column(int p_offset = 0) const; - virtual int get_token_line_indent(int p_offset = 0) const; - virtual int get_token_line_tab_indent(int p_offset = 0) const; - virtual const Variant &get_token_constant(int p_offset = 0) const; - virtual String get_token_error(int p_offset = 0) const; - virtual void advance(int p_amount = 1); -#ifdef DEBUG_ENABLED - virtual const Vector<Pair<int, String>> &get_warning_skips() const { return warning_skips; } - virtual const Set<String> &get_warning_global_skips() const { return warning_global_skips; } - virtual bool is_ignoring_warnings() const { return ignore_warnings; } -#endif // DEBUG_ENABLED -}; + Token scan(); -class GDScriptTokenizerBuffer : public GDScriptTokenizer { - enum { + void set_source_code(const String &p_source_code); - TOKEN_BYTE_MASK = 0x80, - TOKEN_BITS = 8, - TOKEN_MASK = (1 << TOKEN_BITS) - 1, - TOKEN_LINE_BITS = 24, - TOKEN_LINE_MASK = (1 << TOKEN_LINE_BITS) - 1, - }; + int get_cursor_line() const; + int get_cursor_column() const; + void set_cursor_position(int p_line, int p_column); + void set_multiline_mode(bool p_state); + bool is_past_cursor() const; + static String get_token_name(Token::Type p_token_type); - Vector<StringName> identifiers; - Vector<Variant> constants; - VMap<uint32_t, uint32_t> lines; - Vector<uint32_t> tokens; - Variant nil; - int token; - -public: - Error set_code_buffer(const Vector<uint8_t> &p_buffer); - static Vector<uint8_t> parse_code_string(const String &p_code); - virtual Token get_token(int p_offset = 0) const; - virtual StringName get_token_identifier(int p_offset = 0) const; - virtual GDScriptFunctions::Function get_token_built_in_func(int p_offset = 0) const; - virtual Variant::Type get_token_type(int p_offset = 0) const; - virtual int get_token_line(int p_offset = 0) const; - virtual int get_token_column(int p_offset = 0) const; - virtual int get_token_line_indent(int p_offset = 0) const; - virtual int get_token_line_tab_indent(int p_offset = 0) const { return 0; } - virtual const Variant &get_token_constant(int p_offset = 0) const; - virtual String get_token_error(int p_offset = 0) const; - virtual void advance(int p_amount = 1); -#ifdef DEBUG_ENABLED - virtual const Vector<Pair<int, String>> &get_warning_skips() const { - static Vector<Pair<int, String>> v; - return v; - } - virtual const Set<String> &get_warning_global_skips() const { - static Set<String> s; - return s; - } - virtual bool is_ignoring_warnings() const { return true; } -#endif // DEBUG_ENABLED - GDScriptTokenizerBuffer(); + GDScriptTokenizer(); }; -#endif // GDSCRIPT_TOKENIZER_H +#endif diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp new file mode 100644 index 0000000000..105facd9d0 --- /dev/null +++ b/modules/gdscript/gdscript_warning.cpp @@ -0,0 +1,210 @@ +/*************************************************************************/ +/* gdscript_warning.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#include "gdscript_warning.h" + +#include "core/variant.h" + +#ifdef DEBUG_ENABLED + +String GDScriptWarning::get_message() const { +#define CHECK_SYMBOLS(m_amount) ERR_FAIL_COND_V(symbols.size() < m_amount, String()); + + switch (code) { + case UNASSIGNED_VARIABLE_OP_ASSIGN: { + CHECK_SYMBOLS(1); + return "Using assignment with operation but the variable '" + symbols[0] + "' was not previously assigned a value."; + } break; + case UNASSIGNED_VARIABLE: { + CHECK_SYMBOLS(1); + return "The variable '" + symbols[0] + "' was used but never assigned a value."; + } break; + case UNUSED_VARIABLE: { + CHECK_SYMBOLS(1); + return "The local variable '" + symbols[0] + "' is declared but never used in the block. If this is intended, prefix it with an underscore: '_" + symbols[0] + "'"; + } break; + case UNUSED_LOCAL_CONSTANT: { + CHECK_SYMBOLS(1); + return "The local constant '" + symbols[0] + "' is declared but never used in the block. If this is intended, prefix it with an underscore: '_" + symbols[0] + "'"; + } break; + case SHADOWED_VARIABLE: { + CHECK_SYMBOLS(4); + return vformat(R"(The local %s "%s" is shadowing an already-declared %s at line %s.)", symbols[0], symbols[1], symbols[2], symbols[3]); + } break; + case SHADOWED_VARIABLE_BASE_CLASS: { + CHECK_SYMBOLS(4); + return vformat(R"(The local %s "%s" is shadowing an already-declared %s at the base class "%s".)", symbols[0], symbols[1], symbols[2], symbols[3]); + } break; + case UNUSED_PRIVATE_CLASS_VARIABLE: { + CHECK_SYMBOLS(1); + return "The class variable '" + symbols[0] + "' is declared but never used in the script."; + } break; + case UNUSED_PARAMETER: { + CHECK_SYMBOLS(2); + return "The parameter '" + symbols[1] + "' is never used in the function '" + symbols[0] + "'. If this is intended, prefix it with an underscore: '_" + symbols[1] + "'"; + } break; + case UNREACHABLE_CODE: { + CHECK_SYMBOLS(1); + return "Unreachable code (statement after return) in function '" + symbols[0] + "()'."; + } break; + case UNREACHABLE_PATTERN: { + return "Unreachable pattern (pattern after wildcard or bind)."; + } break; + case STANDALONE_EXPRESSION: { + return "Standalone expression (the line has no effect)."; + } break; + case VOID_ASSIGNMENT: { + CHECK_SYMBOLS(1); + return "Assignment operation, but the function '" + symbols[0] + "()' returns void."; + } break; + case NARROWING_CONVERSION: { + return "Narrowing conversion (float is converted to int and loses precision)."; + } break; + case INCOMPATIBLE_TERNARY: { + return "Values of the ternary conditional are not mutually compatible."; + } break; + case UNUSED_SIGNAL: { + CHECK_SYMBOLS(1); + return "The signal '" + symbols[0] + "' is declared but never emitted."; + } break; + case RETURN_VALUE_DISCARDED: { + CHECK_SYMBOLS(1); + return "The function '" + symbols[0] + "()' returns a value, but this value is never used."; + } break; + case PROPERTY_USED_AS_FUNCTION: { + CHECK_SYMBOLS(2); + return "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a property with the same name. Did you mean to access it?"; + } break; + case CONSTANT_USED_AS_FUNCTION: { + CHECK_SYMBOLS(2); + return "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a constant with the same name. Did you mean to access it?"; + } break; + case FUNCTION_USED_AS_PROPERTY: { + CHECK_SYMBOLS(2); + return "The property '" + symbols[0] + "' was not found in base '" + symbols[1] + "' but there's a method with the same name. Did you mean to call it?"; + } break; + case INTEGER_DIVISION: { + return "Integer division, decimal part will be discarded."; + } break; + case UNSAFE_PROPERTY_ACCESS: { + CHECK_SYMBOLS(2); + return "The property '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype)."; + } break; + case UNSAFE_METHOD_ACCESS: { + CHECK_SYMBOLS(2); + return "The method '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype)."; + } break; + case UNSAFE_CAST: { + CHECK_SYMBOLS(1); + return "The value is cast to '" + symbols[0] + "' but has an unknown type."; + } break; + case UNSAFE_CALL_ARGUMENT: { + CHECK_SYMBOLS(4); + return "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided"; + } break; + case DEPRECATED_KEYWORD: { + CHECK_SYMBOLS(2); + return "The '" + symbols[0] + "' keyword is deprecated and will be removed in a future release, please replace its uses by '" + symbols[1] + "'."; + } break; + case STANDALONE_TERNARY: { + return "Standalone ternary conditional operator: the return value is being discarded."; + } + case ASSERT_ALWAYS_TRUE: { + return "Assert statement is redundant because the expression is always true."; + } + case ASSERT_ALWAYS_FALSE: { + return "Assert statement will raise an error because the expression is always false."; + } + case REDUNDANT_AWAIT: { + return R"("await" keyword not needed in this case, because the expression isn't a coroutine nor a signal.)"; + } + case WARNING_MAX: + break; // Can't happen, but silences warning + } + ERR_FAIL_V_MSG(String(), "Invalid GDScript warning code: " + get_name_from_code(code) + "."); + +#undef CHECK_SYMBOLS +} + +String GDScriptWarning::get_name() const { + return get_name_from_code(code); +} + +String GDScriptWarning::get_name_from_code(Code p_code) { + ERR_FAIL_COND_V(p_code < 0 || p_code >= WARNING_MAX, String()); + + static const char *names[] = { + "UNASSIGNED_VARIABLE", + "UNASSIGNED_VARIABLE_OP_ASSIGN", + "UNUSED_VARIABLE", + "UNUSED_LOCAL_CONSTANT", + "SHADOWED_VARIABLE", + "SHADOWED_VARIABLE_BASE_CLASS", + "UNUSED_PRIVATE_CLASS_VARIABLE", + "UNUSED_PARAMETER", + "UNREACHABLE_CODE", + "UNREACHABLE_PATTERN", + "STANDALONE_EXPRESSION", + "VOID_ASSIGNMENT", + "NARROWING_CONVERSION", + "INCOMPATIBLE_TERNARY", + "UNUSED_SIGNAL", + "RETURN_VALUE_DISCARDED", + "PROPERTY_USED_AS_FUNCTION", + "CONSTANT_USED_AS_FUNCTION", + "FUNCTION_USED_AS_PROPERTY", + "INTEGER_DIVISION", + "UNSAFE_PROPERTY_ACCESS", + "UNSAFE_METHOD_ACCESS", + "UNSAFE_CAST", + "UNSAFE_CALL_ARGUMENT", + "DEPRECATED_KEYWORD", + "STANDALONE_TERNARY", + "ASSERT_ALWAYS_TRUE", + "ASSERT_ALWAYS_FALSE", + "REDUNDANT_AWAIT", + }; + + static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names."); + + return names[(int)p_code]; +} + +GDScriptWarning::Code GDScriptWarning::get_code_from_name(const String &p_name) { + for (int i = 0; i < WARNING_MAX; i++) { + if (get_name_from_code((Code)i) == p_name) { + return (Code)i; + } + } + + ERR_FAIL_V_MSG(WARNING_MAX, "Invalid GDScript warning name: " + p_name); +} + +#endif // DEBUG_ENABLED diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h new file mode 100644 index 0000000000..e183d6f302 --- /dev/null +++ b/modules/gdscript/gdscript_warning.h @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* gdscript_warning.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef GDSCRIPT_WARNINGS +#define GDSCRIPT_WARNINGS + +#ifdef DEBUG_ENABLED + +#include "core/ustring.h" +#include "core/vector.h" + +class GDScriptWarning { +public: + enum Code { + UNASSIGNED_VARIABLE, // Variable used but never assigned. + UNASSIGNED_VARIABLE_OP_ASSIGN, // Variable never assigned but used in an assignment operation (+=, *=, etc). + UNUSED_VARIABLE, // Local variable is declared but never used. + UNUSED_LOCAL_CONSTANT, // Local constant is declared but never used. + SHADOWED_VARIABLE, // Variable name shadowed by other variable in same class. + SHADOWED_VARIABLE_BASE_CLASS, // Variable name shadowed by other variable in some base class. + UNUSED_PRIVATE_CLASS_VARIABLE, // Class variable is declared private ("_" prefix) but never used in the file. + UNUSED_PARAMETER, // Function parameter is never used. + UNREACHABLE_CODE, // Code after a return statement. + UNREACHABLE_PATTERN, // Pattern in a match statement after a catch all pattern (wildcard or bind). + STANDALONE_EXPRESSION, // Expression not assigned to a variable. + VOID_ASSIGNMENT, // Function returns void but it's assigned to a variable. + NARROWING_CONVERSION, // Float value into an integer slot, precision is lost. + INCOMPATIBLE_TERNARY, // Possible values of a ternary if are not mutually compatible. + UNUSED_SIGNAL, // Signal is defined but never emitted. + RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used. + PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name. + CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name. + FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name. + INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded. + UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes). + UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes). + UNSAFE_CAST, // Cast used in an unknown type. + UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument. + DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced. + STANDALONE_TERNARY, // Return value of ternary expression is discarded. + ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true. + ASSERT_ALWAYS_FALSE, // Expression for assert argument is always false. + REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine). + WARNING_MAX, + }; + + Code code = WARNING_MAX; + int start_line = -1, end_line = -1; + int leftmost_column = -1, rightmost_column = -1; + Vector<String> symbols; + + String get_name() const; + String get_message() const; + static String get_name_from_code(Code p_code); + static Code get_code_from_name(const String &p_name); +}; + +#endif // DEBUG_ENABLED + +#endif // GDSCRIPT_WARNINGS diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index 330530be80..4d79d9d395 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -31,6 +31,7 @@ #include "gdscript_extend_parser.h" #include "../gdscript.h" +#include "../gdscript_analyzer.h" #include "core/io/json.h" #include "gdscript_language_protocol.h" #include "gdscript_workspace.h" @@ -38,15 +39,17 @@ void ExtendGDScriptParser::update_diagnostics() { diagnostics.clear(); - if (has_error()) { + const List<ParserError> &errors = get_errors(); + for (const List<ParserError>::Element *E = errors.front(); E != nullptr; E = E->next()) { + const ParserError &error = E->get(); lsp::Diagnostic diagnostic; diagnostic.severity = lsp::DiagnosticSeverity::Error; - diagnostic.message = get_error(); + diagnostic.message = error.message; diagnostic.source = "gdscript"; diagnostic.code = -1; lsp::Range range; lsp::Position pos; - int line = LINE_NUMBER_TO_INDEX(get_error_line()); + int line = LINE_NUMBER_TO_INDEX(error.line); const String &line_text = get_lines()[line]; pos.line = line; pos.character = line_text.length() - line_text.strip_edges(true, false).length(); @@ -62,12 +65,12 @@ void ExtendGDScriptParser::update_diagnostics() { const GDScriptWarning &warning = E->get(); lsp::Diagnostic diagnostic; diagnostic.severity = lsp::DiagnosticSeverity::Warning; - diagnostic.message = warning.get_message(); + diagnostic.message = "(" + warning.get_name() + "): " + warning.get_message(); diagnostic.source = "gdscript"; diagnostic.code = warning.code; lsp::Range range; lsp::Position pos; - int line = LINE_NUMBER_TO_INDEX(warning.line); + int line = LINE_NUMBER_TO_INDEX(warning.start_line); const String &line_text = get_lines()[line]; pos.line = line; pos.character = line_text.length() - line_text.strip_edges(true, false).length(); @@ -82,7 +85,7 @@ void ExtendGDScriptParser::update_diagnostics() { void ExtendGDScriptParser::update_symbols() { members.clear(); - const GDScriptParser::Node *head = get_parse_tree(); + const GDScriptParser::Node *head = get_tree(); if (const GDScriptParser::ClassNode *gdclass = dynamic_cast<const GDScriptParser::ClassNode *>(head)) { parse_class_symbol(gdclass, class_symbol); @@ -106,15 +109,15 @@ void ExtendGDScriptParser::update_symbols() { void ExtendGDScriptParser::update_document_links(const String &p_code) { document_links.clear(); - GDScriptTokenizerText tokenizer; + GDScriptTokenizer tokenizer; FileAccessRef fs = FileAccess::create(FileAccess::ACCESS_RESOURCES); - tokenizer.set_code(p_code); + tokenizer.set_source_code(p_code); while (true) { - GDScriptTokenizerText::Token token = tokenizer.get_token(); - if (token == GDScriptTokenizer::TK_EOF || token == GDScriptTokenizer::TK_ERROR) { + GDScriptTokenizer::Token token = tokenizer.scan(); + if (token.type == GDScriptTokenizer::Token::TK_EOF) { break; - } else if (token == GDScriptTokenizer::TK_CONSTANT) { - const Variant &const_val = tokenizer.get_token_constant(); + } else if (token.type == GDScriptTokenizer::Token::LITERAL) { + const Variant &const_val = token.literal; if (const_val.get_type() == Variant::STRING) { String path = const_val; bool exists = fs->file_exists(path); @@ -126,15 +129,14 @@ void ExtendGDScriptParser::update_document_links(const String &p_code) { String value = const_val; lsp::DocumentLink link; link.target = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(path); - link.range.start.line = LINE_NUMBER_TO_INDEX(tokenizer.get_token_line()); - link.range.end.line = link.range.start.line; - link.range.end.character = LINE_NUMBER_TO_INDEX(tokenizer.get_token_column()); - link.range.start.character = link.range.end.character - value.length(); + link.range.start.line = LINE_NUMBER_TO_INDEX(token.start_line); + link.range.end.line = LINE_NUMBER_TO_INDEX(token.end_line); + link.range.start.character = LINE_NUMBER_TO_INDEX(token.start_column); + link.range.end.character = LINE_NUMBER_TO_INDEX(token.end_column); document_links.push_back(link); } } } - tokenizer.advance(); } } @@ -144,219 +146,238 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p r_symbol.uri = uri; r_symbol.script_path = path; r_symbol.children.clear(); - r_symbol.name = p_class->name; + r_symbol.name = p_class->identifier != nullptr ? String(p_class->identifier->name) : String(); if (r_symbol.name.empty()) { r_symbol.name = path.get_file(); } r_symbol.kind = lsp::SymbolKind::Class; r_symbol.deprecated = false; - r_symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_class->line); - r_symbol.range.start.character = p_class->column; + r_symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_class->start_line); + r_symbol.range.start.character = LINE_NUMBER_TO_INDEX(p_class->start_column); r_symbol.range.end.line = LINE_NUMBER_TO_INDEX(p_class->end_line); r_symbol.selectionRange.start.line = r_symbol.range.start.line; r_symbol.detail = "class " + r_symbol.name; bool is_root_class = &r_symbol == &class_symbol; - r_symbol.documentation = parse_documentation(is_root_class ? 0 : LINE_NUMBER_TO_INDEX(p_class->line), is_root_class); - - for (int i = 0; i < p_class->variables.size(); ++i) { - const GDScriptParser::ClassNode::Member &m = p_class->variables[i]; - - lsp::DocumentSymbol symbol; - symbol.name = m.identifier; - symbol.kind = lsp::SymbolKind::Variable; - symbol.deprecated = false; - const int line = LINE_NUMBER_TO_INDEX(m.line); - symbol.range.start.line = line; - symbol.range.start.character = lines[line].length() - lines[line].strip_edges(true, false).length(); - symbol.range.end.line = line; - symbol.range.end.character = lines[line].length(); - symbol.selectionRange.start.line = symbol.range.start.line; - if (m._export.type != Variant::NIL) { - symbol.detail += "export "; - } - symbol.detail += "var " + m.identifier; - if (m.data_type.kind != GDScriptParser::DataType::UNRESOLVED) { - symbol.detail += ": " + m.data_type.to_string(); - } - if (m.default_value.get_type() != Variant::NIL) { - symbol.detail += " = " + JSON::print(m.default_value); - } - - symbol.documentation = parse_documentation(line); - symbol.uri = uri; - symbol.script_path = path; - - r_symbol.children.push_back(symbol); - } - - for (int i = 0; i < p_class->_signals.size(); ++i) { - const GDScriptParser::ClassNode::Signal &signal = p_class->_signals[i]; - - lsp::DocumentSymbol symbol; - symbol.name = signal.name; - symbol.kind = lsp::SymbolKind::Event; - symbol.deprecated = false; - const int line = LINE_NUMBER_TO_INDEX(signal.line); - symbol.range.start.line = line; - symbol.range.start.character = lines[line].length() - lines[line].strip_edges(true, false).length(); - symbol.range.end.line = symbol.range.start.line; - symbol.range.end.character = lines[line].length(); - symbol.selectionRange.start.line = symbol.range.start.line; - symbol.documentation = parse_documentation(line); - symbol.uri = uri; - symbol.script_path = path; - symbol.detail = "signal " + signal.name + "("; - for (int j = 0; j < signal.arguments.size(); j++) { - if (j > 0) { - symbol.detail += ", "; - } - symbol.detail += signal.arguments[j]; - } - symbol.detail += ")"; - - r_symbol.children.push_back(symbol); - } + r_symbol.documentation = parse_documentation(is_root_class ? 0 : LINE_NUMBER_TO_INDEX(p_class->start_line), is_root_class); + + for (int i = 0; i < p_class->members.size(); i++) { + const ClassNode::Member &m = p_class->members[i]; + + switch (m.type) { + case ClassNode::Member::VARIABLE: { + lsp::DocumentSymbol symbol; + symbol.name = m.variable->identifier->name; + symbol.kind = lsp::SymbolKind::Variable; + symbol.deprecated = false; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.variable->start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.variable->start_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.variable->end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.variable->end_column); + symbol.selectionRange.start.line = symbol.range.start.line; + if (m.variable->exported) { + symbol.detail += "@export "; + } + symbol.detail += "var " + m.variable->identifier->name; + if (m.get_datatype().is_hard_type()) { + symbol.detail += ": " + m.get_datatype().to_string(); + } + if (m.variable->initializer != nullptr && m.variable->initializer->is_constant) { + symbol.detail += " = " + JSON::print(m.variable->initializer->reduced_value); + } - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { - lsp::DocumentSymbol symbol; - const GDScriptParser::ClassNode::Constant &c = E->value(); - const GDScriptParser::ConstantNode *node = dynamic_cast<const GDScriptParser::ConstantNode *>(c.expression); - ERR_FAIL_COND(!node); - symbol.name = E->key(); - symbol.kind = lsp::SymbolKind::Constant; - symbol.deprecated = false; - const int line = LINE_NUMBER_TO_INDEX(E->get().expression->line); - symbol.range.start.line = line; - symbol.range.start.character = E->get().expression->column; - symbol.range.end.line = symbol.range.start.line; - symbol.range.end.character = lines[line].length(); - symbol.selectionRange.start.line = symbol.range.start.line; - symbol.documentation = parse_documentation(line); - symbol.uri = uri; - symbol.script_path = path; + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.variable->start_line)); + symbol.uri = uri; + symbol.script_path = path; + + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::CONSTANT: { + lsp::DocumentSymbol symbol; + + symbol.name = m.constant->identifier->name; + symbol.kind = lsp::SymbolKind::Constant; + symbol.deprecated = false; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.constant->start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.constant->start_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.constant->end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.constant->start_column); + symbol.selectionRange.start.line = LINE_NUMBER_TO_INDEX(m.constant->start_line); + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.constant->start_line)); + symbol.uri = uri; + symbol.script_path = path; + + symbol.detail = "const " + symbol.name; + if (m.constant->get_datatype().is_hard_type()) { + symbol.detail += ": " + m.constant->get_datatype().to_string(); + } - symbol.detail = "const " + symbol.name; - if (c.type.kind != GDScriptParser::DataType::UNRESOLVED) { - symbol.detail += ": " + c.type.to_string(); - } + const Variant &default_value = m.constant->initializer->reduced_value; + String value_text; + if (default_value.get_type() == Variant::OBJECT) { + RES res = default_value; + if (res.is_valid() && !res->get_path().empty()) { + value_text = "preload(\"" + res->get_path() + "\")"; + if (symbol.documentation.empty()) { + if (Map<String, ExtendGDScriptParser *>::Element *S = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(res->get_path())) { + symbol.documentation = S->get()->class_symbol.documentation; + } + } + } else { + value_text = JSON::print(default_value); + } + } else { + value_text = JSON::print(default_value); + } + if (!value_text.empty()) { + symbol.detail += " = " + value_text; + } - String value_text; - if (node->value.get_type() == Variant::OBJECT) { - RES res = node->value; - if (res.is_valid() && !res->get_path().empty()) { - value_text = "preload(\"" + res->get_path() + "\")"; - if (symbol.documentation.empty()) { - if (Map<String, ExtendGDScriptParser *>::Element *S = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(res->get_path())) { - symbol.documentation = S->get()->class_symbol.documentation; + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::ENUM_VALUE: { + lsp::DocumentSymbol symbol; + + symbol.name = m.enum_value.identifier->name; + symbol.kind = lsp::SymbolKind::EnumMember; + symbol.deprecated = false; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.enum_value.leftmost_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.enum_value.rightmost_column); + symbol.selectionRange.start.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.enum_value.line)); + symbol.uri = uri; + symbol.script_path = path; + + symbol.detail = symbol.name + " = " + itos(m.enum_value.value); + + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::SIGNAL: { + lsp::DocumentSymbol symbol; + symbol.name = m.signal->identifier->name; + symbol.kind = lsp::SymbolKind::Event; + symbol.deprecated = false; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.signal->start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.signal->start_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.signal->end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.signal->end_column); + symbol.selectionRange.start.line = symbol.range.start.line; + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.signal->start_line)); + symbol.uri = uri; + symbol.script_path = path; + symbol.detail = "signal " + String(m.signal->identifier->name) + "("; + for (int j = 0; j < m.signal->parameters.size(); j++) { + if (j > 0) { + symbol.detail += ", "; } + symbol.detail += m.signal->parameters[i]->identifier->name; } - } else { - value_text = JSON::print(node->value); - } - } else { - value_text = JSON::print(node->value); - } - if (!value_text.empty()) { - symbol.detail += " = " + value_text; + symbol.detail += ")"; + + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::ENUM: { + lsp::DocumentSymbol symbol; + symbol.kind = lsp::SymbolKind::Enum; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.m_enum->start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.m_enum->start_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.m_enum->end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.m_enum->end_column); + symbol.selectionRange.start.line = symbol.range.start.line; + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.m_enum->start_line)); + symbol.uri = uri; + symbol.script_path = path; + + symbol.detail = "enum " + String(m.m_enum->identifier->name) + "{"; + for (int j = 0; j < m.m_enum->values.size(); j++) { + if (j > 0) { + symbol.detail += ", "; + } + symbol.detail += String(m.m_enum->values[j].identifier->name) + " = " + itos(m.m_enum->values[j].value); + } + symbol.detail += "}"; + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::FUNCTION: { + lsp::DocumentSymbol symbol; + parse_function_symbol(m.function, symbol); + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::CLASS: { + lsp::DocumentSymbol symbol; + parse_class_symbol(m.m_class, symbol); + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::UNDEFINED: + break; // Unreachable. } - - r_symbol.children.push_back(symbol); - } - - for (int i = 0; i < p_class->functions.size(); ++i) { - const GDScriptParser::FunctionNode *func = p_class->functions[i]; - lsp::DocumentSymbol symbol; - parse_function_symbol(func, symbol); - r_symbol.children.push_back(symbol); - } - - for (int i = 0; i < p_class->static_functions.size(); ++i) { - const GDScriptParser::FunctionNode *func = p_class->static_functions[i]; - lsp::DocumentSymbol symbol; - parse_function_symbol(func, symbol); - r_symbol.children.push_back(symbol); - } - - for (int i = 0; i < p_class->subclasses.size(); ++i) { - const GDScriptParser::ClassNode *subclass = p_class->subclasses[i]; - lsp::DocumentSymbol symbol; - parse_class_symbol(subclass, symbol); - r_symbol.children.push_back(symbol); } } void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionNode *p_func, lsp::DocumentSymbol &r_symbol) { const String uri = get_uri(); - r_symbol.name = p_func->name; + r_symbol.name = p_func->identifier->name; r_symbol.kind = lsp::SymbolKind::Function; - r_symbol.detail = "func " + p_func->name + "("; + r_symbol.detail = "func " + String(p_func->identifier->name) + "("; r_symbol.deprecated = false; - const int line = LINE_NUMBER_TO_INDEX(p_func->line); - r_symbol.range.start.line = line; - r_symbol.range.start.character = p_func->column; - r_symbol.range.end.line = MAX(p_func->body->end_line - 2, r_symbol.range.start.line); - r_symbol.range.end.character = lines[r_symbol.range.end.line].length(); + r_symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_func->start_line); + r_symbol.range.start.character = LINE_NUMBER_TO_INDEX(p_func->start_column); + r_symbol.range.end.line = LINE_NUMBER_TO_INDEX(p_func->start_line); + r_symbol.range.end.character = LINE_NUMBER_TO_INDEX(p_func->end_column); r_symbol.selectionRange.start.line = r_symbol.range.start.line; - r_symbol.documentation = parse_documentation(line); + r_symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(p_func->start_line)); r_symbol.uri = uri; r_symbol.script_path = path; - String arguments; - for (int i = 0; i < p_func->arguments.size(); i++) { + String parameters; + for (int i = 0; i < p_func->parameters.size(); i++) { + const ParameterNode *parameter = p_func->parameters[i]; lsp::DocumentSymbol symbol; symbol.kind = lsp::SymbolKind::Variable; - symbol.name = p_func->arguments[i]; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_func->body->line); - symbol.range.start.character = p_func->body->column; - symbol.range.end = symbol.range.start; + symbol.name = parameter->identifier->name; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(parameter->start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(parameter->start_line); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(parameter->end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(parameter->end_column); symbol.uri = uri; symbol.script_path = path; r_symbol.children.push_back(symbol); if (i > 0) { - arguments += ", "; + parameters += ", "; } - arguments += String(p_func->arguments[i]); - if (p_func->argument_types[i].kind != GDScriptParser::DataType::UNRESOLVED) { - arguments += ": " + p_func->argument_types[i].to_string(); + parameters += String(parameter->identifier->name); + if (parameter->get_datatype().is_hard_type()) { + parameters += ": " + parameter->get_datatype().to_string(); } - int default_value_idx = i - (p_func->arguments.size() - p_func->default_values.size()); - if (default_value_idx >= 0) { - const GDScriptParser::ConstantNode *const_node = dynamic_cast<const GDScriptParser::ConstantNode *>(p_func->default_values[default_value_idx]); - if (const_node == nullptr) { - const GDScriptParser::OperatorNode *operator_node = dynamic_cast<const GDScriptParser::OperatorNode *>(p_func->default_values[default_value_idx]); - if (operator_node) { - const_node = dynamic_cast<const GDScriptParser::ConstantNode *>(operator_node->next); - } - } - - if (const_node) { - String value = JSON::print(const_node->value); - arguments += " = " + value; - } + if (parameter->default_value != nullptr) { + String value = JSON::print(parameter->default_value->reduced_value); + parameters += " = " + value; } } - r_symbol.detail += arguments + ")"; - if (p_func->return_type.kind != GDScriptParser::DataType::UNRESOLVED) { - r_symbol.detail += " -> " + p_func->return_type.to_string(); + r_symbol.detail += parameters + ")"; + if (p_func->get_datatype().is_hard_type()) { + r_symbol.detail += " -> " + p_func->get_datatype().to_string(); } - for (const Map<StringName, LocalVarNode *>::Element *E = p_func->body->variables.front(); E; E = E->next()) { + for (int i = 0; i < p_func->body->locals.size(); i++) { + const SuiteNode::Local &local = p_func->body->locals[i]; lsp::DocumentSymbol symbol; - const GDScriptParser::LocalVarNode *var = E->value(); - symbol.name = E->key(); - symbol.kind = lsp::SymbolKind::Variable; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(E->get()->line); - symbol.range.start.character = E->get()->column; - symbol.range.end.line = symbol.range.start.line; - symbol.range.end.character = lines[symbol.range.end.line].length(); + symbol.name = local.name; + symbol.kind = local.type == SuiteNode::Local::CONSTANT ? lsp::SymbolKind::Constant : lsp::SymbolKind::Variable; + symbol.range.start.line = LINE_NUMBER_TO_INDEX(local.start_line); + symbol.range.start.character = LINE_NUMBER_TO_INDEX(local.start_column); + symbol.range.end.line = LINE_NUMBER_TO_INDEX(local.end_line); + symbol.range.end.character = LINE_NUMBER_TO_INDEX(local.end_column); symbol.uri = uri; symbol.script_path = path; - symbol.detail = "var " + symbol.name; - if (var->datatype.kind != GDScriptParser::DataType::UNRESOLVED) { - symbol.detail += ": " + var->datatype.to_string(); + symbol.detail = SuiteNode::Local::CONSTANT ? "const " : "var "; + symbol.detail += symbol.name; + if (local.get_datatype().is_hard_type()) { + symbol.detail += ": " + local.get_datatype().to_string(); } - symbol.documentation = parse_documentation(line); + symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(local.start_line)); r_symbol.children.push_back(symbol); } } @@ -624,34 +645,24 @@ const Array &ExtendGDScriptParser::get_member_completions() { Dictionary ExtendGDScriptParser::dump_function_api(const GDScriptParser::FunctionNode *p_func) const { Dictionary func; ERR_FAIL_NULL_V(p_func, func); - func["name"] = p_func->name; - func["return_type"] = p_func->return_type.to_string(); + func["name"] = p_func->identifier->name; + func["return_type"] = p_func->get_datatype().to_string(); func["rpc_mode"] = p_func->rpc_mode; - Array arguments; - for (int i = 0; i < p_func->arguments.size(); i++) { + Array parameters; + for (int i = 0; i < p_func->parameters.size(); i++) { Dictionary arg; - arg["name"] = p_func->arguments[i]; - arg["type"] = p_func->argument_types[i].to_string(); - int default_value_idx = i - (p_func->arguments.size() - p_func->default_values.size()); - if (default_value_idx >= 0) { - const GDScriptParser::ConstantNode *const_node = dynamic_cast<const GDScriptParser::ConstantNode *>(p_func->default_values[default_value_idx]); - if (const_node == nullptr) { - const GDScriptParser::OperatorNode *operator_node = dynamic_cast<const GDScriptParser::OperatorNode *>(p_func->default_values[default_value_idx]); - if (operator_node) { - const_node = dynamic_cast<const GDScriptParser::ConstantNode *>(operator_node->next); - } - } - if (const_node) { - arg["default_value"] = const_node->value; - } + arg["name"] = p_func->parameters[i]->identifier->name; + arg["type"] = p_func->parameters[i]->get_datatype().to_string(); + if (p_func->parameters[i]->default_value != nullptr) { + arg["default_value"] = p_func->parameters[i]->default_value->reduced_value; } - arguments.push_back(arg); + parameters.push_back(arg); } - if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_func->line))) { + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_func->start_line))) { func["signature"] = symbol->detail; func["description"] = symbol->documentation; } - func["arguments"] = arguments; + func["arguments"] = parameters; return func; } @@ -660,91 +671,117 @@ Dictionary ExtendGDScriptParser::dump_class_api(const GDScriptParser::ClassNode ERR_FAIL_NULL_V(p_class, class_api); - class_api["name"] = String(p_class->name); + class_api["name"] = p_class->identifier != nullptr ? String(p_class->identifier->name) : String(); class_api["path"] = path; Array extends_class; - for (int i = 0; i < p_class->extends_class.size(); i++) { - extends_class.append(String(p_class->extends_class[i])); + for (int i = 0; i < p_class->extends.size(); i++) { + extends_class.append(String(p_class->extends[i])); } class_api["extends_class"] = extends_class; - class_api["extends_file"] = String(p_class->extends_file); + class_api["extends_file"] = String(p_class->extends_path); class_api["icon"] = String(p_class->icon_path); - if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_class->line))) { + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(p_class->start_line))) { class_api["signature"] = symbol->detail; class_api["description"] = symbol->documentation; } - Array subclasses; - for (int i = 0; i < p_class->subclasses.size(); i++) { - subclasses.push_back(dump_class_api(p_class->subclasses[i])); - } - class_api["sub_classes"] = subclasses; - + Array nested_classes; Array constants; - for (Map<StringName, GDScriptParser::ClassNode::Constant>::Element *E = p_class->constant_expressions.front(); E; E = E->next()) { - const GDScriptParser::ClassNode::Constant &c = E->value(); - const GDScriptParser::ConstantNode *node = dynamic_cast<const GDScriptParser::ConstantNode *>(c.expression); - ERR_FAIL_COND_V(!node, class_api); - - Dictionary api; - api["name"] = E->key(); - api["value"] = node->value; - api["data_type"] = node->datatype.to_string(); - if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(node->line))) { - api["signature"] = symbol->detail; - api["description"] = symbol->documentation; - } - constants.push_back(api); - } - class_api["constants"] = constants; - Array members; - for (int i = 0; i < p_class->variables.size(); ++i) { - const GDScriptParser::ClassNode::Member &m = p_class->variables[i]; - Dictionary api; - api["name"] = m.identifier; - api["data_type"] = m.data_type.to_string(); - api["default_value"] = m.default_value; - api["setter"] = String(m.setter); - api["getter"] = String(m.getter); - api["export"] = m._export.type != Variant::NIL; - if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.line))) { - api["signature"] = symbol->detail; - api["description"] = symbol->documentation; - } - members.push_back(api); - } - class_api["members"] = members; - Array signals; - for (int i = 0; i < p_class->_signals.size(); ++i) { - const GDScriptParser::ClassNode::Signal &signal = p_class->_signals[i]; - Dictionary api; - api["name"] = signal.name; - Array args; - for (int j = 0; j < signal.arguments.size(); j++) { - args.append(signal.arguments[j]); - } - api["arguments"] = args; - if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(signal.line))) { - api["signature"] = symbol->detail; - api["description"] = symbol->documentation; + Array methods; + Array static_functions; + + for (int i = 0; i < p_class->members.size(); i++) { + const ClassNode::Member &m = p_class->members[i]; + switch (m.type) { + case ClassNode::Member::CLASS: + nested_classes.push_back(dump_class_api(m.m_class)); + break; + case ClassNode::Member::CONSTANT: { + Dictionary api; + api["name"] = m.constant->identifier->name; + api["value"] = m.constant->initializer->reduced_value; + api["data_type"] = m.constant->get_datatype().to_string(); + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.constant->start_line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + constants.push_back(api); + } break; + case ClassNode::Member::ENUM_VALUE: { + Dictionary api; + api["name"] = m.enum_value.identifier->name; + api["value"] = m.enum_value.value; + api["data_type"] = m.get_datatype().to_string(); + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.enum_value.line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + constants.push_back(api); + } break; + case ClassNode::Member::ENUM: { + Dictionary enum_dict; + for (int j = 0; j < m.m_enum->values.size(); i++) { + enum_dict[m.m_enum->values[i].identifier->name] = m.m_enum->values[i].value; + } + + Dictionary api; + api["name"] = m.m_enum->identifier->name; + api["value"] = enum_dict; + api["data_type"] = m.get_datatype().to_string(); + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.m_enum->start_line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + constants.push_back(api); + } break; + case ClassNode::Member::VARIABLE: { + Dictionary api; + api["name"] = m.variable->identifier->name; + api["data_type"] = m.variable->get_datatype().to_string(); + api["default_value"] = m.variable->initializer != nullptr ? m.variable->initializer->reduced_value : Variant(); + api["setter"] = m.variable->setter ? ("@" + String(m.variable->identifier->name) + "_setter") : (m.variable->setter_pointer != nullptr ? String(m.variable->setter_pointer->name) : String()); + api["getter"] = m.variable->getter ? ("@" + String(m.variable->identifier->name) + "_getter") : (m.variable->getter_pointer != nullptr ? String(m.variable->getter_pointer->name) : String()); + api["export"] = m.variable->exported; + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.variable->start_line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + members.push_back(api); + } break; + case ClassNode::Member::SIGNAL: { + Dictionary api; + api["name"] = m.signal->identifier->name; + Array pars; + for (int j = 0; j < m.signal->parameters.size(); j++) { + pars.append(String(m.signal->parameters[i]->identifier->name)); + } + api["arguments"] = pars; + if (const lsp::DocumentSymbol *symbol = get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(m.signal->start_line))) { + api["signature"] = symbol->detail; + api["description"] = symbol->documentation; + } + signals.push_back(api); + } break; + case ClassNode::Member::FUNCTION: { + if (m.function->is_static) { + static_functions.append(dump_function_api(m.function)); + } else { + methods.append(dump_function_api(m.function)); + } + } break; + case ClassNode::Member::UNDEFINED: + break; // Unreachable. } - signals.push_back(api); } - class_api["signals"] = signals; - Array methods; - for (int i = 0; i < p_class->functions.size(); ++i) { - methods.append(dump_function_api(p_class->functions[i])); - } + class_api["sub_classes"] = nested_classes; + class_api["constants"] = constants; + class_api["members"] = members; + class_api["signals"] = signals; class_api["methods"] = methods; - - Array static_functions; - for (int i = 0; i < p_class->static_functions.size(); ++i) { - static_functions.append(dump_function_api(p_class->static_functions[i])); - } class_api["static_functions"] = static_functions; return class_api; @@ -752,7 +789,7 @@ Dictionary ExtendGDScriptParser::dump_class_api(const GDScriptParser::ClassNode Dictionary ExtendGDScriptParser::generate_api() const { Dictionary api; - const GDScriptParser::Node *head = get_parse_tree(); + const GDScriptParser::Node *head = get_tree(); if (const GDScriptParser::ClassNode *gdclass = dynamic_cast<const GDScriptParser::ClassNode *>(head)) { api = dump_class_api(gdclass); } @@ -763,7 +800,11 @@ Error ExtendGDScriptParser::parse(const String &p_code, const String &p_path) { path = p_path; lines = p_code.split("\n"); - Error err = GDScriptParser::parse(p_code, p_path.get_base_dir(), false, p_path, false, nullptr, false); + Error err = GDScriptParser::parse(p_code, p_path, false); + if (err == OK) { + GDScriptAnalyzer analyzer(this); + err = analyzer.analyze(); + } update_diagnostics(); update_symbols(); update_document_links(p_code); diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index a203b9bfdb..776193e37c 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -118,7 +118,7 @@ void GDScriptWorkspace::reload_all_workspace_scripts() { Map<String, ExtendGDScriptParser *>::Element *S = parse_results.find(path); String err_msg = "Failed parse script " + path; if (S) { - err_msg += "\n" + S->get()->get_error(); + err_msg += "\n" + S->get()->get_errors()[0].message; } ERR_CONTINUE_MSG(err != OK, err_msg); } diff --git a/modules/gdscript/register_types.cpp b/modules/gdscript/register_types.cpp index 884946efc1..c554cbac05 100644 --- a/modules/gdscript/register_types.cpp +++ b/modules/gdscript/register_types.cpp @@ -35,11 +35,14 @@ #include "core/os/dir_access.h" #include "core/os/file_access.h" #include "gdscript.h" +#include "gdscript_analyzer.h" +#include "gdscript_cache.h" #include "gdscript_tokenizer.h" GDScriptLanguage *script_language_gd = nullptr; Ref<ResourceFormatLoaderGDScript> resource_loader_gd; Ref<ResourceFormatSaverGDScript> resource_saver_gd; +GDScriptCache *gdscript_cache = nullptr; #ifdef TOOLS_ENABLED @@ -76,64 +79,8 @@ public: return; } - Vector<uint8_t> file = FileAccess::get_file_as_array(p_path); - if (file.empty()) { - return; - } - - String txt; - txt.parse_utf8((const char *)file.ptr(), file.size()); - file = GDScriptTokenizerBuffer::parse_code_string(txt); - - if (!file.empty()) { - if (script_mode == EditorExportPreset::MODE_SCRIPT_ENCRYPTED) { - String tmp_path = EditorSettings::get_singleton()->get_cache_dir().plus_file("script.gde"); - FileAccess *fa = FileAccess::open(tmp_path, FileAccess::WRITE); - - Vector<uint8_t> key; - key.resize(32); - for (int i = 0; i < 32; i++) { - int v = 0; - if (i * 2 < script_key.length()) { - CharType ct = script_key[i * 2]; - if (ct >= '0' && ct <= '9') { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct << 4; - } - - if (i * 2 + 1 < script_key.length()) { - CharType ct = script_key[i * 2 + 1]; - if (ct >= '0' && ct <= '9') { - ct = ct - '0'; - } else if (ct >= 'a' && ct <= 'f') { - ct = 10 + ct - 'a'; - } - v |= ct; - } - key.write[i] = v; - } - FileAccessEncrypted *fae = memnew(FileAccessEncrypted); - Error err = fae->open_and_parse(fa, key, FileAccessEncrypted::MODE_WRITE_AES256); - - if (err == OK) { - fae->store_buffer(file.ptr(), file.size()); - } - - memdelete(fae); - - file = FileAccess::get_file_as_array(tmp_path); - add_file(p_path.get_basename() + ".gde", file, true); - - // Clean up temporary file. - DirAccess::remove_file_or_error(tmp_path); - - } else { - add_file(p_path.get_basename() + ".gdc", file, true); - } - } + // TODO: Readd compiled/encrypted GDScript on export. + return; } }; @@ -142,6 +89,12 @@ static void _editor_init() { gd_export.instance(); EditorExport::get_singleton()->add_export_plugin(gd_export); +#ifdef TOOLS_ENABLED + Ref<GDScriptSyntaxHighlighter> gdscript_syntax_highlighter; + gdscript_syntax_highlighter.instance(); + ScriptEditor::get_singleton()->register_syntax_highlighter(gdscript_syntax_highlighter); +#endif + #ifndef GDSCRIPT_NO_LSP register_lsp_types(); GDScriptLanguageServer *lsp_plugin = memnew(GDScriptLanguageServer); @@ -165,8 +118,9 @@ void register_gdscript_types() { resource_saver_gd.instance(); ResourceSaver::add_resource_format_saver(resource_saver_gd); + gdscript_cache = memnew(GDScriptCache); + #ifdef TOOLS_ENABLED - ScriptEditor::register_create_syntax_highlighter_function(GDScriptSyntaxHighlighter::create); EditorNode::add_init_callback(_editor_init); gdscript_translation_parser_plugin.instance(); @@ -177,6 +131,10 @@ void register_gdscript_types() { void unregister_gdscript_types() { ScriptServer::unregister_language(script_language_gd); + if (gdscript_cache) { + memdelete(gdscript_cache); + } + if (script_language_gd) { memdelete(script_language_gd); } @@ -191,4 +149,7 @@ void unregister_gdscript_types() { EditorTranslationParser::get_singleton()->remove_parser(gdscript_translation_parser_plugin, EditorTranslationParser::STANDARD); gdscript_translation_parser_plugin.unref(); #endif // TOOLS_ENABLED + + GDScriptParser::cleanup(); + GDScriptAnalyzer::cleanup(); } diff --git a/modules/mobile_vr/register_types.cpp b/modules/mobile_vr/register_types.cpp index 75638d47c4..0bb555e780 100644 --- a/modules/mobile_vr/register_types.cpp +++ b/modules/mobile_vr/register_types.cpp @@ -35,9 +35,11 @@ void register_mobile_vr_types() { ClassDB::register_class<MobileVRInterface>(); - Ref<MobileVRInterface> mobile_vr; - mobile_vr.instance(); - XRServer::get_singleton()->add_interface(mobile_vr); + if (XRServer::get_singleton()) { + Ref<MobileVRInterface> mobile_vr; + mobile_vr.instance(); + XRServer::get_singleton()->add_interface(mobile_vr); + } } void unregister_mobile_vr_types() { diff --git a/modules/modules_builders.py b/modules/modules_builders.py index e7be6380d1..2243162555 100644 --- a/modules/modules_builders.py +++ b/modules/modules_builders.py @@ -12,5 +12,16 @@ def generate_modules_enabled(target, source, env): f.write("#define %s\n" % ("MODULE_" + module.upper() + "_ENABLED")) +def generate_modules_tests(target, source, env): + import os + import glob + + with open(target[0].path, "w") as f: + for name, path in env.module_list.items(): + headers = glob.glob(os.path.join(path, "tests", "*.h")) + for h in headers: + f.write('#include "%s"\n' % (os.path.normpath(h))) + + if __name__ == "__main__": subprocess_main(globals()) diff --git a/modules/mono/build_scripts/mono_configure.py b/modules/mono/build_scripts/mono_configure.py index 80e3b59325..3e771e06f0 100644 --- a/modules/mono/build_scripts/mono_configure.py +++ b/modules/mono/build_scripts/mono_configure.py @@ -125,7 +125,8 @@ def configure(env, env_mono): if not mono_prefix and (os.getenv("MONO32_PREFIX") or os.getenv("MONO64_PREFIX")): print( - "WARNING: The environment variables 'MONO32_PREFIX' and 'MONO64_PREFIX' are deprecated; use the 'mono_prefix' SCons parameter instead" + "WARNING: The environment variables 'MONO32_PREFIX' and 'MONO64_PREFIX' are deprecated; use the" + " 'mono_prefix' SCons parameter instead" ) # Although we don't support building with tools for any platform where we currently use static AOT, diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 22e4d84e98..bbdec224f0 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -44,7 +44,6 @@ #ifdef TOOLS_ENABLED #include "editor/bindings_generator.h" -#include "editor/csharp_project.h" #include "editor/editor_node.h" #include "editor/node_dock.h" #endif @@ -897,7 +896,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { // Call OnBeforeSerialize if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { - obj->get_script_instance()->call_multilevel(string_names.on_before_serialize); + obj->get_script_instance()->call(string_names.on_before_serialize); } // Save instance info @@ -1133,7 +1132,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { // Call OnAfterDeserialization if (csi->script->script_class->implements_interface(CACHED_CLASS(ISerializationListener))) { - obj->get_script_instance()->call_multilevel(string_names.on_after_deserialize); + obj->get_script_instance()->call(string_names.on_after_deserialize); } } } @@ -1866,41 +1865,6 @@ Variant CSharpInstance::call(const StringName &p_method, const Variant **p_args, return Variant(); } -void CSharpInstance::call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount) { - GD_MONO_SCOPE_THREAD_ATTACH; - - if (script.is_valid()) { - MonoObject *mono_object = get_mono_object(); - - ERR_FAIL_NULL(mono_object); - - _call_multilevel(mono_object, p_method, p_args, p_argcount); - } -} - -void CSharpInstance::_call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount) { - GD_MONO_ASSERT_THREAD_ATTACHED; - - GDMonoClass *top = script->script_class; - - while (top && top != script->native) { - GDMonoMethod *method = top->get_method(p_method, p_argcount); - - if (method) { - method->invoke(p_mono_object, p_args); - return; - } - - top = top->get_parent_class(); - } -} - -void CSharpInstance::call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount) { - // Sorry, the method is the one that controls the call order - - call_multilevel(p_method, p_args, p_argcount); -} - bool CSharpInstance::_reference_owner_unsafe() { #ifdef DEBUG_ENABLED CRASH_COND(!base_ref); @@ -1963,7 +1927,7 @@ MonoObject *CSharpInstance::_internal_new_managed() { bool die = _unreference_owner_unsafe(); // Not ok for the owner to die here. If there is a situation where this can happen, it will be considered a bug. - CRASH_COND(die == true); + CRASH_COND(die); owner = nullptr; @@ -2298,7 +2262,7 @@ CSharpInstance::~CSharpInstance() { // Unreference the owner here, before the new "instance binding" references it. // Otherwise, the unsafe reference debug checks will incorrectly detect a bug. bool die = _unreference_owner_unsafe(); - CRASH_COND(die == true); // `owner_keep_alive` holds a reference, so it can't die + CRASH_COND(die); // `owner_keep_alive` holds a reference, so it can't die void *data = owner->get_script_instance_binding(CSharpLanguage::get_singleton()->get_language_index()); CRASH_COND(data == nullptr); @@ -3136,7 +3100,7 @@ CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_arg bool die = instance->_unreference_owner_unsafe(); // Not ok for the owner to die here. If there is a situation where this can happen, it will be considered a bug. - CRASH_COND(die == true); + CRASH_COND(die); p_owner->set_script_instance(nullptr); r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; @@ -3759,13 +3723,9 @@ Error ResourceFormatSaverCSharpScript::save(const String &p_path, const RES &p_r #ifdef TOOLS_ENABLED if (!FileAccess::exists(p_path)) { - // The file does not yet exists, let's assume the user just created this script - - if (_create_project_solution_if_needed()) { - CSharpProject::add_item(GodotSharpDirs::get_project_csproj_path(), - "Compile", - ProjectSettings::get_singleton()->globalize_path(p_path)); - } else { + // The file does not yet exist, let's assume the user just created this script. In such + // cases we need to check whether the solution and csproj were already created or not. + if (!_create_project_solution_if_needed()) { ERR_PRINT("C# project could not be created; cannot add file: '" + p_path + "'."); } } diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index c2370364f9..f0b43a40f9 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -265,8 +265,6 @@ class CSharpInstance : public ScriptInstance { friend void GDMonoInternals::tie_managed_to_unmanaged(MonoObject *, Object *); static CSharpInstance *create_for_managed_type(Object *p_owner, CSharpScript *p_script, const MonoGCHandleData &p_gchandle); - void _call_multilevel(MonoObject *p_mono_object, const StringName &p_method, const Variant **p_args, int p_argcount); - void get_properties_state_for_reloading(List<Pair<StringName, Variant>> &r_state); void get_event_signals_state_for_reloading(List<Pair<StringName, Array>> &r_state); @@ -285,8 +283,6 @@ public: /* TODO */ void get_method_list(List<MethodInfo> *p_list) const override {} bool has_method(const StringName &p_method) const override; Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; - void call_multilevel(const StringName &p_method, const Variant **p_args, int p_argcount) override; - void call_multilevel_reversed(const StringName &p_method, const Variant **p_args, int p_argcount) override; void mono_object_disposed(MonoObject *p_obj); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln new file mode 100644 index 0000000000..56c0cb7703 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj new file mode 100644 index 0000000000..86a0a4393e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -0,0 +1,35 @@ +<Project Sdk="Microsoft.Build.NoTargets/2.0.1"> + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + + <Description>MSBuild .NET Sdk for Godot projects.</Description> + <Authors>Godot Engine contributors</Authors> + + <PackageId>Godot.NET.Sdk</PackageId> + <Version>4.0.0</Version> + <PackageVersion>4.0.0-dev2</PackageVersion> + <PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl> + <PackageType>MSBuildSdk</PackageType> + <PackageTags>MSBuildSdk</PackageTags> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + </PropertyGroup> + + <PropertyGroup> + <NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile> + <GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn> + </PropertyGroup> + + <Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') "> + <PropertyGroup> + <NuspecProperties> + id=$(PackageId); + description=$(Description); + authors=$(Authors); + version=$(PackageVersion); + packagetype=$(PackageType); + tags=$(PackageTags); + projecturl=$(PackageProjectUrl) + </NuspecProperties> + </PropertyGroup> + </Target> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec new file mode 100644 index 0000000000..5b5cefe80e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.nuspec @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8" ?> +<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd"> + <metadata> + <id>$id$</id> + <version>$version$</version> + <description>$description$</description> + <authors>$authors$</authors> + <owners>$authors$</owners> + <projectUrl>$projecturl$</projectUrl> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <license type="expression">MIT</license> + <licenseUrl>https://licenses.nuget.org/MIT</licenseUrl> + <tags>$tags$</tags> + <packageTypes> + <packageType name="$packagetype$" /> + </packageTypes> + <repository url="$projecturl$" /> + </metadata> + <files> + <file src="Sdk\**" target="Sdk" />\ + </files> +</package> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props new file mode 100644 index 0000000000..dfc59e6ccb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -0,0 +1,112 @@ +<Project> + <PropertyGroup> + <!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. --> + <GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk> + + <GodotProjectTypeGuid>{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}</GodotProjectTypeGuid> + </PropertyGroup> + + <PropertyGroup> + <Configurations>Debug;ExportDebug;ExportRelease</Configurations> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + + <GodotProjectDir Condition=" '$(SolutionDir)' != '' ">$(SolutionDir)</GodotProjectDir> + <GodotProjectDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildProjectDirectory)</GodotProjectDir> + <GodotProjectDir>$([MSBuild]::EnsureTrailingSlash('$(GodotProjectDir)'))</GodotProjectDir> + + <!-- Custom output paths for Godot projects. In brief, 'bin\' and 'obj\' are moved to '$(GodotProjectDir)\.mono\temp\'. --> + <BaseOutputPath>$(GodotProjectDir).mono\temp\bin\</BaseOutputPath> + <OutputPath>$(GodotProjectDir).mono\temp\bin\$(Configuration)\</OutputPath> + <!-- + Use custom IntermediateOutputPath and BaseIntermediateOutputPath only if it wasn't already set. + Otherwise the old values may have already been changed by MSBuild which can cause problems with NuGet. + --> + <IntermediateOutputPath Condition=" '$(IntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\$(Configuration)\</IntermediateOutputPath> + <BaseIntermediateOutputPath Condition=" '$(BaseIntermediateOutputPath)' == '' ">$(GodotProjectDir).mono\temp\obj\</BaseIntermediateOutputPath> + + <!-- Do not append the target framework name to the output path. --> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> + </PropertyGroup> + + <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " /> + + <PropertyGroup> + <EnableDefaultNoneItems>false</EnableDefaultNoneItems> + </PropertyGroup> + + <!-- + The Microsoft.NET.Sdk only understands of the Debug and Release configurations. + We need to set the following properties manually for ExportDebug and ExportRelease. + --> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' or '$(Configuration)' == 'ExportDebug' "> + <DebugSymbols Condition=" '$(DebugSymbols)' == '' ">true</DebugSymbols> + <Optimize Condition=" '$(Optimize)' == '' ">false</Optimize> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'ExportRelease' "> + <Optimize Condition=" '$(Optimize)' == '' ">true</Optimize> + </PropertyGroup> + + <PropertyGroup> + <GodotApiConfiguration Condition=" '$(Configuration)' != 'ExportRelease' ">Debug</GodotApiConfiguration> + <GodotApiConfiguration Condition=" '$(Configuration)' == 'ExportRelease' ">Release</GodotApiConfiguration> + </PropertyGroup> + + <!-- Auto-detect the target Godot platform if it was not specified. --> + <PropertyGroup Condition=" '$(GodotTargetPlatform)' == '' "> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Linux))' ">linuxbsd</GodotTargetPlatform> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(FreeBSD))' ">linuxbsd</GodotTargetPlatform> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(OSX))' ">osx</GodotTargetPlatform> + <GodotTargetPlatform Condition=" '$([MSBuild]::IsOsPlatform(Windows))' ">windows</GodotTargetPlatform> + </PropertyGroup> + + <PropertyGroup> + <GodotRealTIsDouble Condition=" '$(GodotRealTIsDouble)' == '' ">false</GodotRealTIsDouble> + </PropertyGroup> + + <!-- Godot DefineConstants. --> + <PropertyGroup> + <!-- Define constant to identify Godot builds. --> + <GodotDefineConstants>GODOT</GodotDefineConstants> + + <!-- + Define constant to determine the target Godot platform. This includes the + recognized platform names and the platform category (PC, MOBILE or WEB). + --> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'windows' ">GODOT_WINDOWS;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'linuxbsd' ">GODOT_LINUXBSD;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'osx' ">GODOT_OSX;GODOT_MACOS;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'server' ">GODOT_SERVER;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'uwp' ">GODOT_UWP;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'haiku' ">GODOT_HAIKU;GODOT_PC</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'android' ">GODOT_ANDROID;GODOT_MOBILE</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'iphone' ">GODOT_IPHONE;GODOT_IOS;GODOT_MOBILE</GodotPlatformConstants> + <GodotPlatformConstants Condition=" '$(GodotTargetPlatform)' == 'javascript' ">GODOT_JAVASCRIPT;GODOT_HTML5;GODOT_WASM;GODOT_WEB</GodotPlatformConstants> + + <GodotDefineConstants>$(GodotDefineConstants);$(GodotPlatformConstants)</GodotDefineConstants> + </PropertyGroup> + + <PropertyGroup> + <!-- ExportDebug also defines DEBUG like Debug does. --> + <DefineConstants Condition=" '$(Configuration)' == 'ExportDebug' ">$(DefineConstants);DEBUG</DefineConstants> + <!-- Debug defines TOOLS to differenciate between Debug and ExportDebug configurations. --> + <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">$(DefineConstants);TOOLS</DefineConstants> + + <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> + </PropertyGroup> + + <ItemGroup> + <!-- + TODO: + We should consider a nuget package for reference assemblies. This is difficult because the + Godot scripting API is continuaslly breaking backwards compatibility even in patch releases. + --> + <Reference Include="GodotSharp"> + <Private>false</Private> + <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharp.dll</HintPath> + </Reference> + <Reference Include="GodotSharpEditor" Condition=" '$(Configuration)' == 'Debug' "> + <Private>false</Private> + <HintPath>$(GodotProjectDir).mono\assemblies\$(GodotApiConfiguration)\GodotSharpEditor.dll</HintPath> + </Reference> + </ItemGroup> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets new file mode 100644 index 0000000000..f5afd75505 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.targets @@ -0,0 +1,17 @@ +<Project> + <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" Condition=" '$(GodotSdkImportsMicrosoftNetSdk)' == 'true' " /> + + <PropertyGroup> + <EnableGodotProjectTypeGuid Condition=" '$(EnableGodotProjectTypeGuid)' == '' ">true</EnableGodotProjectTypeGuid> + <ProjectTypeGuids Condition=" '$(EnableGodotProjectTypeGuid)' == 'true' ">$(GodotProjectTypeGuid);$(DefaultProjectTypeGuid)</ProjectTypeGuids> + </PropertyGroup> + + <PropertyGroup> + <!-- + Define constant to determine whether the real_t type in Godot is double precision or not. + By default this is false, like the official Godot builds. If someone is using a custom + Godot build where real_t is double, they can override the GodotRealTIsDouble property. + --> + <DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants> + </PropertyGroup> +</Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index c2549b4ad5..5edf72d63e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -70,13 +70,14 @@ namespace GodotTools.BuildLogger { string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}"; - if (e.ProjectFile.Length > 0) + if (!string.IsNullOrEmpty(e.ProjectFile)) line += $" [{e.ProjectFile}]"; WriteLine(line); string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + - $@"{e.Code.CsvEscape()},{e.Message.CsvEscape()},{e.ProjectFile.CsvEscape()}"; + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(errorLine); } @@ -89,8 +90,9 @@ namespace GodotTools.BuildLogger WriteLine(line); - string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber},{e.Code.CsvEscape()}," + - $@"{e.Message.CsvEscape()},{(e.ProjectFile != null ? e.ProjectFile.CsvEscape() : string.Empty)}"; + string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(warningLine); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs index 85760a3705..e1ccf0454a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/FileUtils.cs @@ -19,7 +19,10 @@ namespace GodotTools.Core } if (attempt > maxAttempts + 1) - return; + { + // Overwrite the oldest one + backupPath = backupPathBase; + } File.Copy(filePath, backupPath, overwrite: true); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 012b69032e..e6b0e8f1df 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -43,7 +43,7 @@ namespace GodotTools.Core path.StartsWith(DriveRoot, StringComparison.Ordinal); } - public static string ToSafeDirName(this string dirName, bool allowDirSeparator) + public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false) { var invalidChars = new List<string> { ":", "*", "?", "\"", "<", ">", "|" }; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs index 99a55c471b..4db71500da 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Reflection; diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index affb2a47e7..ce2b378623 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 9cb50014b0..41c94f19c8 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -11,13 +11,21 @@ <ItemGroup> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> </ItemGroup> + <!-- + The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described + here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 + We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when + searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. + --> <ItemGroup> - <!-- - The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described - here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 - We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when - searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. - --> <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> </ItemGroup> + <Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition="$([MSBuild]::IsOsPlatform(Windows))"> + <PropertyGroup> + <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> + <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> + </PropertyGroup> + <!-- Need to copy it here as well on Windows --> + <Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" /> + </Target> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs index f93eb9a1fa..ed77076df3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/IdentifierUtils.cs @@ -22,6 +22,37 @@ namespace GodotTools.ProjectEditor return string.Join(".", identifiers); } + /// <summary> + /// Skips invalid identifier characters including decimal digit numbers at the start of the identifier. + /// </summary> + private static void SkipInvalidCharacters(string source, int startIndex, StringBuilder outputBuilder) + { + for (int i = startIndex; i < source.Length; i++) + { + char @char = source[i]; + + switch (char.GetUnicodeCategory(@char)) + { + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.TitlecaseLetter: + case UnicodeCategory.ModifierLetter: + case UnicodeCategory.LetterNumber: + case UnicodeCategory.OtherLetter: + outputBuilder.Append(@char); + break; + case UnicodeCategory.NonSpacingMark: + case UnicodeCategory.SpacingCombiningMark: + case UnicodeCategory.ConnectorPunctuation: + case UnicodeCategory.DecimalDigitNumber: + // Identifiers may start with underscore + if (outputBuilder.Length > startIndex || @char == '_') + outputBuilder.Append(@char); + break; + } + } + } + public static string SanitizeIdentifier(string identifier, bool allowEmpty) { if (string.IsNullOrEmpty(identifier)) @@ -44,30 +75,7 @@ namespace GodotTools.ProjectEditor startIndex += 1; } - for (int i = startIndex; i < identifier.Length; i++) - { - char @char = identifier[i]; - - switch (Char.GetUnicodeCategory(@char)) - { - case UnicodeCategory.UppercaseLetter: - case UnicodeCategory.LowercaseLetter: - case UnicodeCategory.TitlecaseLetter: - case UnicodeCategory.ModifierLetter: - case UnicodeCategory.LetterNumber: - case UnicodeCategory.OtherLetter: - identifierBuilder.Append(@char); - break; - case UnicodeCategory.NonSpacingMark: - case UnicodeCategory.SpacingCombiningMark: - case UnicodeCategory.ConnectorPunctuation: - case UnicodeCategory.DecimalDigitNumber: - // Identifiers may start with underscore - if (identifierBuilder.Length > startIndex || @char == '_') - identifierBuilder.Append(@char); - break; - } - } + SkipInvalidCharacters(identifier, startIndex, identifierBuilder); if (identifierBuilder.Length == startIndex) { diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs deleted file mode 100644 index 704f2ec194..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectExtensions.cs +++ /dev/null @@ -1,118 +0,0 @@ -using GodotTools.Core; -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Build.Construction; -using Microsoft.Build.Globbing; - -namespace GodotTools.ProjectEditor -{ - public static class ProjectExtensions - { - public static ProjectItemElement FindItemOrNull(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - string normalizedInclude = include.NormalizePath(); - - foreach (var itemGroup in root.ItemGroups) - { - if (noCondition && itemGroup.Condition.Length != 0) - continue; - - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - //var glob = Glob.Parse(item.Include.NormalizePath(), globOptions); - var glob = MSBuildGlob.Parse(item.Include.NormalizePath()); - - if (glob.IsMatch(normalizedInclude)) - return item; - } - } - - return null; - } - public static ProjectItemElement FindItemOrNullAbs(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - string normalizedInclude = Path.GetFullPath(include).NormalizePath(); - - foreach (var itemGroup in root.ItemGroups) - { - if (noCondition && itemGroup.Condition.Length != 0) - continue; - - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - var glob = MSBuildGlob.Parse(Path.GetFullPath(item.Include).NormalizePath()); - - if (glob.IsMatch(normalizedInclude)) - return item; - } - } - - return null; - } - - public static IEnumerable<ProjectItemElement> FindAllItemsInFolder(this ProjectRootElement root, string itemType, string folder) - { - string absFolderNormalizedWithSep = Path.GetFullPath(folder).NormalizePath() + Path.DirectorySeparatorChar; - - foreach (var itemGroup in root.ItemGroups) - { - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath(); - - if (absPathNormalized.StartsWith(absFolderNormalizedWithSep)) - yield return item; - } - } - } - - public static bool HasItem(this ProjectRootElement root, string itemType, string include, bool noCondition = false) - { - return root.FindItemOrNull(itemType, include, noCondition) != null; - } - - public static bool AddItemChecked(this ProjectRootElement root, string itemType, string include) - { - if (!root.HasItem(itemType, include, noCondition: true)) - { - root.AddItem(itemType, include); - return true; - } - - return false; - } - - public static bool RemoveItemChecked(this ProjectRootElement root, string itemType, string include) - { - var item = root.FindItemOrNullAbs(itemType, include); - if (item != null) - { - item.Parent.RemoveChild(item); - return true; - } - - return false; - } - - public static Guid GetGuid(this ProjectRootElement root) - { - foreach (var property in root.Properties) - { - if (property.Name == "ProjectGuid") - return Guid.Parse(property.Value); - } - - return Guid.Empty; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 679d5bb444..01d7c99662 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,174 +1,51 @@ -using GodotTools.Core; using System; -using System.Collections.Generic; using System.IO; -using System.Reflection; +using System.Text; using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; namespace GodotTools.ProjectEditor { public static class ProjectGenerator { - private const string CoreApiProjectName = "GodotSharp"; - private const string EditorApiProjectName = "GodotSharpEditor"; + public const string GodotSdkVersionToUse = "4.0.0-dev2"; - public const string CSharpProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; - public const string GodotProjectTypeGuid = "{8F3E2DF0-C35C-4265-82FC-BEA011F4A7ED}"; + public static string GodotSdkAttrValue => $"Godot.NET.Sdk/{GodotSdkVersionToUse}"; - public static readonly string GodotDefaultProjectTypeGuids = $"{GodotProjectTypeGuid};{CSharpProjectTypeGuid}"; - - public static string GenGameProject(string dir, string name, IEnumerable<string> compileItems) + public static ProjectRootElement GenGameProject(string name) { - string path = Path.Combine(dir, name + ".csproj"); - - ProjectPropertyGroupElement mainGroup; - var root = CreateLibraryProject(name, "Debug", out mainGroup); - - mainGroup.SetProperty("ProjectTypeGuids", GodotDefaultProjectTypeGuids); - mainGroup.SetProperty("OutputPath", Path.Combine(".mono", "temp", "bin", "$(Configuration)")); - mainGroup.SetProperty("BaseIntermediateOutputPath", Path.Combine(".mono", "temp", "obj")); - mainGroup.SetProperty("IntermediateOutputPath", Path.Combine("$(BaseIntermediateOutputPath)", "$(Configuration)")); - mainGroup.SetProperty("ApiConfiguration", "Debug").Condition = " '$(Configuration)' != 'ExportRelease' "; - mainGroup.SetProperty("ApiConfiguration", "Release").Condition = " '$(Configuration)' == 'ExportRelease' "; - - var debugGroup = root.AddPropertyGroup(); - debugGroup.Condition = " '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "; - debugGroup.AddProperty("DebugSymbols", "true"); - debugGroup.AddProperty("DebugType", "portable"); - debugGroup.AddProperty("Optimize", "false"); - debugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;TOOLS;"); - debugGroup.AddProperty("ErrorReport", "prompt"); - debugGroup.AddProperty("WarningLevel", "4"); - debugGroup.AddProperty("ConsolePause", "false"); - - var coreApiRef = root.AddItem("Reference", CoreApiProjectName); - coreApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", CoreApiProjectName + ".dll")); - coreApiRef.AddMetadata("Private", "False"); - - var editorApiRef = root.AddItem("Reference", EditorApiProjectName); - editorApiRef.Condition = " '$(Configuration)' == 'Debug' "; - editorApiRef.AddMetadata("HintPath", Path.Combine("$(ProjectDir)", ".mono", "assemblies", "$(ApiConfiguration)", EditorApiProjectName + ".dll")); - editorApiRef.AddMetadata("Private", "False"); - - GenAssemblyInfoFile(root, dir, name); - - foreach (var item in compileItems) - { - root.AddItem("Compile", item.RelativeToPath(dir).Replace("/", "\\")); - } - - root.Save(path); - - return root.GetGuid().ToString().ToUpper(); - } - - private static void GenAssemblyInfoFile(ProjectRootElement root, string dir, string name, string[] assemblyLines = null, string[] usingDirectives = null) - { - string propertiesDir = Path.Combine(dir, "Properties"); - if (!Directory.Exists(propertiesDir)) - Directory.CreateDirectory(propertiesDir); - - string usingDirectivesText = string.Empty; + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - if (usingDirectives != null) - { - foreach (var usingDirective in usingDirectives) - usingDirectivesText += "\nusing " + usingDirective + ";"; - } + var root = ProjectRootElement.Create(NewProjectFileOptions.None); - string assemblyLinesText = string.Empty; + root.Sdk = GodotSdkAttrValue; - if (assemblyLines != null) - assemblyLinesText += string.Join("\n", assemblyLines) + "\n"; + var mainGroup = root.AddPropertyGroup(); + mainGroup.AddProperty("TargetFramework", "netstandard2.1"); - string content = string.Format(AssemblyInfoTemplate, usingDirectivesText, name, assemblyLinesText); + string sanitizedName = IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true); - string assemblyInfoFile = Path.Combine(propertiesDir, "AssemblyInfo.cs"); + // If the name is not a valid namespace, manually set RootNamespace to a sanitized one. + if (sanitizedName != name) + mainGroup.AddProperty("RootNamespace", sanitizedName); - File.WriteAllText(assemblyInfoFile, content); - - root.AddItem("Compile", assemblyInfoFile.RelativeToPath(dir).Replace("/", "\\")); + return root; } - public static ProjectRootElement CreateLibraryProject(string name, string defaultConfig, out ProjectPropertyGroupElement mainGroup) + public static string GenAndSaveGameProject(string dir, string name) { - if (string.IsNullOrEmpty(name)) - throw new ArgumentException($"{nameof(name)} cannot be empty", nameof(name)); - - var root = ProjectRootElement.Create(); - root.DefaultTargets = "Build"; - - mainGroup = root.AddPropertyGroup(); - mainGroup.AddProperty("Configuration", defaultConfig).Condition = " '$(Configuration)' == '' "; - mainGroup.AddProperty("Platform", "AnyCPU").Condition = " '$(Platform)' == '' "; - mainGroup.AddProperty("ProjectGuid", "{" + Guid.NewGuid().ToString().ToUpper() + "}"); - mainGroup.AddProperty("OutputType", "Library"); - mainGroup.AddProperty("OutputPath", Path.Combine("bin", "$(Configuration)")); - mainGroup.AddProperty("RootNamespace", IdentifierUtils.SanitizeQualifiedIdentifier(name, allowEmptyIdentifiers: true)); - mainGroup.AddProperty("AssemblyName", name); - mainGroup.AddProperty("TargetFrameworkVersion", "v4.7"); - mainGroup.AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); + if (name.Length == 0) + throw new ArgumentException("Project name is empty", nameof(name)); - var exportDebugGroup = root.AddPropertyGroup(); - exportDebugGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportDebug|AnyCPU' "; - exportDebugGroup.AddProperty("DebugSymbols", "true"); - exportDebugGroup.AddProperty("DebugType", "portable"); - exportDebugGroup.AddProperty("Optimize", "false"); - exportDebugGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;DEBUG;"); - exportDebugGroup.AddProperty("ErrorReport", "prompt"); - exportDebugGroup.AddProperty("WarningLevel", "4"); - exportDebugGroup.AddProperty("ConsolePause", "false"); - - var exportReleaseGroup = root.AddPropertyGroup(); - exportReleaseGroup.Condition = " '$(Configuration)|$(Platform)' == 'ExportRelease|AnyCPU' "; - exportReleaseGroup.AddProperty("DebugType", "portable"); - exportReleaseGroup.AddProperty("Optimize", "true"); - exportReleaseGroup.AddProperty("DefineConstants", "$(GodotDefineConstants);GODOT;"); - exportReleaseGroup.AddProperty("ErrorReport", "prompt"); - exportReleaseGroup.AddProperty("WarningLevel", "4"); - exportReleaseGroup.AddProperty("ConsolePause", "false"); - - // References - var referenceGroup = root.AddItemGroup(); - referenceGroup.AddItem("Reference", "System"); - var frameworkRefAssembliesItem = referenceGroup.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); + string path = Path.Combine(dir, name + ".csproj"); - // Use metadata (child nodes) instead of attributes for the PackageReference. - // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. - frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); - frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); + var root = GenGameProject(name); - root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\")); + // Save (without BOM) + root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); - return root; + return Guid.NewGuid().ToString().ToUpper(); } - - private const string AssemblyInfoTemplate = - @"using System.Reflection;{0} - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle(""{1}"")] -[assembly: AssemblyDescription("""")] -[assembly: AssemblyConfiguration("""")] -[assembly: AssemblyCompany("""")] -[assembly: AssemblyProduct("""")] -[assembly: AssemblyCopyright("""")] -[assembly: AssemblyTrademark("""")] -[assembly: AssemblyCulture("""")] - -// The assembly version has the format ""{{Major}}.{{Minor}}.{{Build}}.{{Revision}}"". -// The form ""{{Major}}.{{Minor}}.*"" will automatically update the build and revision, -// and ""{{Major}}.{{Minor}}.{{Build}}.*"" will update just the revision. - -[assembly: AssemblyVersion(""1.0.*"")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("""")] -{2}"; } } diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 8774b4ee31..4041c56597 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,9 +1,9 @@ +using System; using GodotTools.Core; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using Microsoft.Build.Construction; using Microsoft.Build.Globbing; @@ -11,7 +11,7 @@ namespace GodotTools.ProjectEditor { public sealed class MSBuildProject { - public ProjectRootElement Root { get; } + internal ProjectRootElement Root { get; set; } public bool HasUnsavedChanges { get; set; } @@ -31,91 +31,7 @@ namespace GodotTools.ProjectEditor return root != null ? new MSBuildProject(root) : null; } - public static void AddItemToProjectChecked(string projectPath, string itemType, string include) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedInclude = include.RelativeToPath(dir).Replace("/", "\\"); - - if (root.AddItemChecked(itemType, normalizedInclude)) - root.Save(); - } - - public static void RenameItemInProjectChecked(string projectPath, string itemType, string oldInclude, string newInclude) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedOldInclude = oldInclude.NormalizePath(); - var normalizedNewInclude = newInclude.NormalizePath(); - - var item = root.FindItemOrNullAbs(itemType, normalizedOldInclude); - - if (item == null) - return; - - item.Include = normalizedNewInclude.RelativeToPath(dir).Replace("/", "\\"); - root.Save(); - } - - public static void RemoveItemFromProjectChecked(string projectPath, string itemType, string include) - { - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var normalizedInclude = include.NormalizePath(); - - if (root.RemoveItemChecked(itemType, normalizedInclude)) - root.Save(); - } - - public static void RenameItemsToNewFolderInProjectChecked(string projectPath, string itemType, string oldFolder, string newFolder) - { - var dir = Directory.GetParent(projectPath).FullName; - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - bool dirty = false; - - var oldFolderNormalized = oldFolder.NormalizePath(); - var newFolderNormalized = newFolder.NormalizePath(); - string absOldFolderNormalized = Path.GetFullPath(oldFolderNormalized).NormalizePath(); - string absNewFolderNormalized = Path.GetFullPath(newFolderNormalized).NormalizePath(); - - foreach (var item in root.FindAllItemsInFolder(itemType, oldFolderNormalized)) - { - string absPathNormalized = Path.GetFullPath(item.Include).NormalizePath(); - string absNewIncludeNormalized = absNewFolderNormalized + absPathNormalized.Substring(absOldFolderNormalized.Length); - item.Include = absNewIncludeNormalized.RelativeToPath(dir).Replace("/", "\\"); - dirty = true; - } - - if (dirty) - root.Save(); - } - - public static void RemoveItemsInFolderFromProjectChecked(string projectPath, string itemType, string folder) - { - var root = ProjectRootElement.Open(projectPath); - Debug.Assert(root != null); - - var folderNormalized = folder.NormalizePath(); - - var itemsToRemove = root.FindAllItemsInFolder(itemType, folderNormalized).ToList(); - - if (itemsToRemove.Count > 0) - { - foreach (var item in itemsToRemove) - item.Parent.RemoveChild(item); - - root.Save(); - } - } - - private static string[] GetAllFilesRecursive(string rootDirectory, string mask) + private static List<string> GetAllFilesRecursive(string rootDirectory, string mask) { string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories); @@ -125,262 +41,59 @@ namespace GodotTools.ProjectEditor files[i] = files[i].RelativeToPath(rootDirectory); } - return files; + return new List<string>(files); } - public static string[] GetIncludeFiles(string projectPath, string itemType) + // NOTE: Assumes auto-including items. Only used by the scripts metadata generator, which will be replaced with source generators in the future. + public static IEnumerable<string> GetIncludeFiles(string projectPath, string itemType) { - var result = new List<string>(); - var existingFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); + var excluded = new List<string>(); + var includedFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs"); var root = ProjectRootElement.Open(projectPath); Debug.Assert(root != null); - foreach (var itemGroup in root.ItemGroups) + foreach (var item in root.Items) { - if (itemGroup.Condition.Length != 0) + if (string.IsNullOrEmpty(item.Condition)) continue; - foreach (var item in itemGroup.Items) - { - if (item.ItemType != itemType) - continue; - - string normalizedInclude = item.Include.NormalizePath(); + if (item.ItemType != itemType) + continue; - var glob = MSBuildGlob.Parse(normalizedInclude); + string normalizedExclude = item.Exclude.NormalizePath(); - // TODO Check somehow if path has no blob to avoid the following loop... + var glob = MSBuildGlob.Parse(normalizedExclude); - foreach (var existingFile in existingFiles) - { - if (glob.IsMatch(existingFile)) - { - result.Add(existingFile); - } - } - } + excluded.AddRange(includedFiles.Where(includedFile => glob.IsMatch(includedFile))); } - return result.ToArray(); + includedFiles.RemoveAll(f => excluded.Contains(f)); + + return includedFiles; } - public static void EnsureHasProjectTypeGuids(MSBuildProject project) + public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName) { - var root = project.Root; - - bool found = root.PropertyGroups.Any(pg => - string.IsNullOrEmpty(pg.Condition) && pg.Properties.Any(p => p.Name == "ProjectTypeGuids")); + var origRoot = project.Root; - if (found) + if (!string.IsNullOrEmpty(origRoot.Sdk)) return; - root.AddProperty("ProjectTypeGuids", ProjectGenerator.GodotDefaultProjectTypeGuids); - + project.Root = ProjectGenerator.GenGameProject(projectName); + project.Root.FullPath = origRoot.FullPath; project.HasUnsavedChanges = true; } - /// Simple function to make sure the Api assembly references are configured correctly - public static void FixApiHintPath(MSBuildProject project) - { - var root = project.Root; - - void AddPropertyIfNotPresent(string name, string condition, string value) - { - if (root.PropertyGroups - .Any(g => (string.IsNullOrEmpty(g.Condition) || g.Condition.Trim() == condition) && - g.Properties - .Any(p => p.Name == name && - p.Value == value && - (p.Condition.Trim() == condition || g.Condition.Trim() == condition)))) - { - return; - } - - root.AddProperty(name, value).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' != 'ExportRelease'", - value: "Debug"); - AddPropertyIfNotPresent(name: "ApiConfiguration", - condition: "'$(Configuration)' == 'ExportRelease'", - value: "Release"); - - void SetReferenceHintPath(string referenceName, string condition, string hintPath) - { - foreach (var itemGroup in root.ItemGroups.Where(g => - g.Condition.Trim() == string.Empty || g.Condition.Trim() == condition)) - { - var references = itemGroup.Items.Where(item => - item.ItemType == "Reference" && - item.Include == referenceName && - (item.Condition.Trim() == condition || itemGroup.Condition.Trim() == condition)); - - var referencesWithHintPath = references.Where(reference => - reference.Metadata.Any(m => m.Name == "HintPath")); - - if (referencesWithHintPath.Any(reference => reference.Metadata - .Any(m => m.Name == "HintPath" && m.Value == hintPath))) - { - // Found a Reference item with the right HintPath - return; - } - - var referenceWithHintPath = referencesWithHintPath.FirstOrDefault(); - if (referenceWithHintPath != null) - { - // Found a Reference item with a wrong HintPath - foreach (var metadata in referenceWithHintPath.Metadata.ToList() - .Where(m => m.Name == "HintPath")) - { - // Safe to remove as we duplicate with ToList() to loop - referenceWithHintPath.RemoveChild(metadata); - } - - referenceWithHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - - var referenceWithoutHintPath = references.FirstOrDefault(); - if (referenceWithoutHintPath != null) - { - // Found a Reference item without a HintPath - referenceWithoutHintPath.AddMetadata("HintPath", hintPath); - project.HasUnsavedChanges = true; - return; - } - } - - // Found no Reference item at all. Add it. - root.AddItem("Reference", referenceName).Condition = " " + condition + " "; - project.HasUnsavedChanges = true; - } - - const string coreProjectName = "GodotSharp"; - const string editorProjectName = "GodotSharpEditor"; - - const string coreCondition = ""; - const string editorCondition = "'$(Configuration)' == 'Debug'"; - - var coreHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{coreProjectName}.dll"; - var editorHintPath = $"$(ProjectDir)/.mono/assemblies/$(ApiConfiguration)/{editorProjectName}.dll"; - - SetReferenceHintPath(coreProjectName, coreCondition, coreHintPath); - SetReferenceHintPath(editorProjectName, editorCondition, editorHintPath); - } - - public static void MigrateFromOldConfigNames(MSBuildProject project) - { - var root = project.Root; - - bool hasGodotProjectGeneratorVersion = false; - bool foundOldConfiguration = false; - - foreach (var propertyGroup in root.PropertyGroups.Where(g => string.IsNullOrEmpty(g.Condition))) - { - if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion")) - hasGodotProjectGeneratorVersion = true; - - foreach (var configItem in propertyGroup.Properties - .Where(p => p.Condition.Trim() == "'$(Configuration)' == ''" && p.Value == "Tools")) - { - configItem.Value = "Debug"; - foundOldConfiguration = true; - project.HasUnsavedChanges = true; - } - } - - if (!hasGodotProjectGeneratorVersion) - { - root.PropertyGroups.First(g => string.IsNullOrEmpty(g.Condition))? - .AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); - project.HasUnsavedChanges = true; - } - - if (!foundOldConfiguration) - { - var toolsConditions = new[] - { - "'$(Configuration)|$(Platform)' == 'Tools|AnyCPU'", - "'$(Configuration)|$(Platform)' != 'Tools|AnyCPU'", - "'$(Configuration)' == 'Tools'", - "'$(Configuration)' != 'Tools'" - }; - - foundOldConfiguration = root.PropertyGroups - .Any(g => toolsConditions.Any(c => c == g.Condition.Trim())); - } - - if (foundOldConfiguration) - { - void MigrateConfigurationConditions(string oldConfiguration, string newConfiguration) - { - void MigrateConditions(string oldCondition, string newCondition) - { - foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - propertyGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var propertyGroup in root.PropertyGroups) - { - foreach (var prop in propertyGroup.Properties.Where(p => p.Condition.Trim() == oldCondition)) - { - prop.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - - foreach (var itemGroup in root.ItemGroups.Where(g => g.Condition.Trim() == oldCondition)) - { - itemGroup.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - - foreach (var itemGroup in root.ItemGroups) - { - foreach (var item in itemGroup.Items.Where(item => item.Condition.Trim() == oldCondition)) - { - item.Condition = " " + newCondition + " "; - project.HasUnsavedChanges = true; - } - } - } - - foreach (var op in new[] {"==", "!="}) - { - MigrateConditions($"'$(Configuration)|$(Platform)' {op} '{oldConfiguration}|AnyCPU'", $"'$(Configuration)|$(Platform)' {op} '{newConfiguration}|AnyCPU'"); - MigrateConditions($"'$(Configuration)' {op} '{oldConfiguration}'", $"'$(Configuration)' {op} '{newConfiguration}'"); - } - } - - MigrateConfigurationConditions("Debug", "ExportDebug"); - MigrateConfigurationConditions("Release", "ExportRelease"); - MigrateConfigurationConditions("Tools", "Debug"); // Must be last - } - } - - public static void EnsureHasNugetNetFrameworkRefAssemblies(MSBuildProject project) + public static void EnsureGodotSdkIsUpToDate(MSBuildProject project) { var root = project.Root; + string godotSdkAttrValue = ProjectGenerator.GodotSdkAttrValue; - bool found = root.ItemGroups.Any(g => string.IsNullOrEmpty(g.Condition) && g.Items.Any( - item => item.ItemType == "PackageReference" && item.Include == "Microsoft.NETFramework.ReferenceAssemblies")); - - if (found) + if (!string.IsNullOrEmpty(root.Sdk) && root.Sdk.Trim().Equals(godotSdkAttrValue, StringComparison.OrdinalIgnoreCase)) return; - var frameworkRefAssembliesItem = root.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); - - // Use metadata (child nodes) instead of attributes for the PackageReference. - // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. - frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); - frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); - + root.Sdk = godotSdkAttrValue; project.HasUnsavedChanges = true; } } diff --git a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs index 3de3d8d318..3ab669a9f3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BottomPanel.cs @@ -24,48 +24,50 @@ namespace GodotTools private Button errorsBtn; private Button viewLogBtn; - private void _UpdateBuildTabsList() + private void _UpdateBuildTab(int index, int? currentTab) { - buildTabsList.Clear(); + var tab = (BuildTab)buildTabs.GetChild(index); - int currentTab = buildTabs.CurrentTab; + string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution); + itemName += " [" + tab.BuildInfo.Configuration + "]"; - bool noCurrentTab = currentTab < 0 || currentTab >= buildTabs.GetTabCount(); + buildTabsList.AddItem(itemName, tab.IconTexture); - for (int i = 0; i < buildTabs.GetChildCount(); i++) - { - var tab = (BuildTab)buildTabs.GetChild(i); + string itemTooltip = "Solution: " + tab.BuildInfo.Solution; + itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration; + itemTooltip += "\nStatus: "; - if (tab == null) - continue; + if (tab.BuildExited) + itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; + else + itemTooltip += "Running"; - string itemName = Path.GetFileNameWithoutExtension(tab.BuildInfo.Solution); - itemName += " [" + tab.BuildInfo.Configuration + "]"; + if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) + itemTooltip += $"\nErrors: {tab.ErrorCount}"; - buildTabsList.AddItem(itemName, tab.IconTexture); + itemTooltip += $"\nWarnings: {tab.WarningCount}"; - string itemTooltip = "Solution: " + tab.BuildInfo.Solution; - itemTooltip += "\nConfiguration: " + tab.BuildInfo.Configuration; - itemTooltip += "\nStatus: "; + buildTabsList.SetItemTooltip(index, itemTooltip); - if (tab.BuildExited) - itemTooltip += tab.BuildResult == BuildTab.BuildResults.Success ? "Succeeded" : "Errored"; - else - itemTooltip += "Running"; + // If this tab was already selected before the changes or if no tab was selected + if (currentTab == null || currentTab == index) + { + buildTabsList.Select(index); + _BuildTabsItemSelected(index); + } + } - if (!tab.BuildExited || tab.BuildResult == BuildTab.BuildResults.Error) - itemTooltip += $"\nErrors: {tab.ErrorCount}"; + private void _UpdateBuildTabsList() + { + buildTabsList.Clear(); - itemTooltip += $"\nWarnings: {tab.WarningCount}"; + int? currentTab = buildTabs.CurrentTab; - buildTabsList.SetItemTooltip(i, itemTooltip); + if (currentTab < 0 || currentTab >= buildTabs.GetTabCount()) + currentTab = null; - if (noCurrentTab || currentTab == i) - { - buildTabsList.Select(i); - _BuildTabsItemSelected(i); - } - } + for (int i = 0; i < buildTabs.GetChildCount(); i++) + _UpdateBuildTab(i, currentTab); } public BuildTab GetBuildTabFor(BuildInfo buildInfo) @@ -160,13 +162,7 @@ namespace GodotTools } } - var godotDefines = new[] - { - OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; - - bool buildSuccess = BuildManager.BuildProjectBlocking("Debug", godotDefines); + bool buildSuccess = BuildManager.BuildProjectBlocking("Debug"); if (!buildSuccess) return; @@ -272,7 +268,7 @@ namespace GodotTools }; panelTabs.AddChild(panelBuildsTab); - var toolBarHBox = new HBoxContainer { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; + var toolBarHBox = new HBoxContainer {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; panelBuildsTab.AddChild(toolBarHBox); var buildProjectBtn = new Button @@ -325,7 +321,7 @@ namespace GodotTools }; panelBuildsTab.AddChild(hsc); - buildTabsList = new ItemList { SizeFlagsHorizontal = (int)SizeFlags.ExpandFill }; + buildTabsList = new ItemList {SizeFlagsHorizontal = (int)SizeFlags.ExpandFill}; buildTabsList.ItemSelected += _BuildTabsItemSelected; buildTabsList.NothingSelected += _BuildTabsNothingSelected; hsc.AddChild(buildTabsList); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 34e42489eb..d9862ae361 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -118,9 +118,14 @@ namespace GodotTools.Build string arguments = string.Empty; if (buildTool == BuildTool.DotnetCli) - arguments += "msbuild "; // `dotnet msbuild` command + arguments += "msbuild"; // `dotnet msbuild` command - arguments += $@"""{buildInfo.Solution}"" /t:{string.Join(",", buildInfo.Targets)} " + + arguments += $@" ""{buildInfo.Solution}"""; + + if (buildInfo.Restore) + arguments += " /restore"; + + arguments += $@" /t:{string.Join(",", buildInfo.Targets)} " + $@"""/p:{"Configuration=" + buildInfo.Configuration}"" /v:normal " + $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{buildInfo.LogsDirPath}"""; diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index 0974d23176..ff7ce97c47 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -6,6 +6,7 @@ using GodotTools.Build; using GodotTools.Ides.Rider; using GodotTools.Internals; using GodotTools.Utils; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; @@ -152,7 +153,7 @@ namespace GodotTools } } - public static bool BuildProjectBlocking(string config, IEnumerable<string> godotDefines) + public static bool BuildProjectBlocking(string config, [CanBeNull] string platform = null) { if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return true; // No solution to build @@ -168,29 +169,18 @@ namespace GodotTools return false; } - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); - using (var pr = new EditorProgress("mono_project_debug_build", "Building project solution...", 1)) { pr.Step("Building project solution", 0); var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Build"}, config, restore: true); - bool escapeNeedsDoubleBackslash = buildTool == BuildTool.MsBuildMono || buildTool == BuildTool.DotnetCli; - - // Add Godot defines - string constants = !escapeNeedsDoubleBackslash ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; - - foreach (var godotDefine in godotDefines) - constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; + // If a platform was not specified, try determining the current one. If that fails, let MSBuild auto-detect it. + if (platform != null || OS.PlatformNameMap.TryGetValue(Godot.OS.GetName(), out platform)) + buildInfo.CustomProperties.Add($"GodotTargetPlatform={platform}"); if (Internal.GodotIsRealTDouble()) - constants += "GODOT_REAL_T_IS_DOUBLE;"; - - constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\""; - - buildInfo.CustomProperties.Add(constants); + buildInfo.CustomProperties.Add("GodotRealTIsDouble=true"); if (!Build(buildInfo)) { @@ -215,31 +205,10 @@ namespace GodotTools if (File.Exists(editorScriptsMetadataPath)) File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings; - - if (currentPlayRequest != null) - { - if (currentPlayRequest.Value.HasDebugger) - { - // Set the environment variable that will tell the player to connect to the IDE debugger - // TODO: We should probably add a better way to do this - Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", - "--debugger-agent=transport=dt_socket" + - $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" + - ",server=n"); - } - - if (!currentPlayRequest.Value.BuildBeforePlaying) - return true; // Requested play from an external editor/IDE which already built the project - } - - var godotDefines = new[] - { - Godot.OS.GetName(), - Internal.GodotIs32Bits() ? "32" : "64" - }; + if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) + return true; // Requested play from an external editor/IDE which already built the project - return BuildProjectBlocking("Debug", godotDefines); + return BuildProjectBlocking("Debug"); } public static void Initialize() diff --git a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs index 421729cc11..a8afb38728 100644 --- a/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs +++ b/modules/mono/editor/GodotTools/GodotTools/CsProjOperations.cs @@ -1,9 +1,9 @@ using Godot; using System; +using System.Linq; using Godot.Collections; using GodotTools.Internals; using GodotTools.ProjectEditor; -using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; using Directory = GodotTools.Utils.Directory; @@ -15,7 +15,7 @@ namespace GodotTools { try { - return ProjectGenerator.GenGameProject(dir, name, compileItems: new string[] { }); + return ProjectGenerator.GenAndSaveGameProject(dir, name); } catch (Exception e) { @@ -24,14 +24,6 @@ namespace GodotTools } } - public static void AddItem(string projectPath, string itemType, string include) - { - if (!(bool)GlobalDef("mono/project/auto_update_project", true)) - return; - - ProjectUtils.AddItemToProjectChecked(projectPath, itemType, include); - } - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static ulong ConvertToTimestamp(this DateTime value) @@ -40,81 +32,77 @@ namespace GodotTools return (ulong)elapsedTime.TotalSeconds; } - public static void GenerateScriptsMetadata(string projectPath, string outputPath) + private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata) { - if (File.Exists(outputPath)) - File.Delete(outputPath); + fileMetadata = null; - var oldDict = Internal.GetScriptsMetadataOrNothing(); - var newDict = new Godot.Collections.Dictionary<string, object>(); + var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr); - foreach (var includeFile in ProjectUtils.GetIncludeFiles(projectPath, "Compile")) + if (parseError != Error.Ok) { - string projectIncludeFile = ("res://" + includeFile).SimplifyGodotPath(); + GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}"); + return false; + } - ulong modifiedTime = File.GetLastWriteTime(projectIncludeFile).ConvertToTimestamp(); + string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile); - if (oldDict.TryGetValue(projectIncludeFile, out var oldFileVar)) - { - var oldFileDict = (Dictionary)oldFileVar; - - if (ulong.TryParse(oldFileDict["modified_time"] as string, out ulong storedModifiedTime)) - { - if (storedModifiedTime == modifiedTime) - { - // No changes so no need to parse again - newDict[projectIncludeFile] = oldFileDict; - continue; - } - } - } + var firstMatch = classes.FirstOrDefault(classDecl => + classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object. + classDecl.SearchName != searchName // Filter by the name we're looking for + ); + + if (firstMatch == null) + return false; // Not found - Error parseError = ScriptClassParser.ParseFile(projectIncludeFile, out var classes, out string errorStr); - if (parseError != Error.Ok) + fileMetadata = new Dictionary + { + ["modified_time"] = $"{modifiedTime}", + ["class"] = new Dictionary { - GD.PushError($"Failed to determine namespace and class for script: {projectIncludeFile}. Parse error: {errorStr ?? parseError.ToString()}"); - continue; + ["namespace"] = firstMatch.Namespace, + ["class_name"] = firstMatch.Name, + ["nested"] = firstMatch.Nested } + }; - string searchName = System.IO.Path.GetFileNameWithoutExtension(projectIncludeFile); - - var classDict = new Dictionary(); + return true; + } - foreach (var classDecl in classes) - { - if (classDecl.BaseCount == 0) - continue; // Does not inherit nor implement anything, so it can't be a script class + public static void GenerateScriptsMetadata(string projectPath, string outputPath) + { + var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate(); - string classCmp = classDecl.Nested ? - classDecl.Name.Substring(classDecl.Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : - classDecl.Name; + bool IsUpToDate(string includeFile, ulong modifiedTime) + { + return metadataDict.TryGetValue(includeFile, out var oldFileVar) && + ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string, + out ulong storedModifiedTime) && storedModifiedTime == modifiedTime; + } - if (classCmp != searchName) - continue; + var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile") + .Select(path => ("res://" + path).SimplifyGodotPath()) + .ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp()) + .Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value)) + .ToArray(); - classDict["namespace"] = classDecl.Namespace; - classDict["class_name"] = classDecl.Name; - classDict["nested"] = classDecl.Nested; - break; - } + foreach (var pair in outdatedFiles) + { + metadataDict.Remove(pair.Key); - if (classDict.Count == 0) - continue; // Not found + string includeFile = pair.Key; - newDict[projectIncludeFile] = new Dictionary { ["modified_time"] = $"{modifiedTime}", ["class"] = classDict }; + if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata)) + metadataDict[includeFile] = fileMetadata; } - if (newDict.Count > 0) - { - string json = JSON.Print(newDict); + string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict); - string baseDir = outputPath.GetBaseDir(); + string baseDir = outputPath.GetBaseDir(); - if (!Directory.Exists(baseDir)) - Directory.CreateDirectory(baseDir); + if (!Directory.Exists(baseDir)) + Directory.CreateDirectory(baseDir); - File.WriteAllText(outputPath, json); - } + File.WriteAllText(outputPath, json); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 6bfbc62f3b..e819848212 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.CompilerServices; using GodotTools.Core; using GodotTools.Internals; +using JetBrains.Annotations; using static GodotTools.Internals.Globals; using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; @@ -145,9 +146,7 @@ namespace GodotTools.Export if (!File.Exists(GodotSharpDirs.ProjectSlnPath)) return; - string platform = DeterminePlatformFromFeatures(features); - - if (platform == null) + if (!DeterminePlatformFromFeatures(features, out string platform)) throw new NotSupportedException("Target platform not supported"); string outputDir = new FileInfo(path).Directory?.FullName ?? @@ -160,10 +159,7 @@ namespace GodotTools.Export AddFile(scriptsMetadataPath, scriptsMetadataPath); - // Turn export features into defines - var godotDefines = features; - - if (!BuildManager.BuildProjectBlocking(buildConfig, godotDefines)) + if (!BuildManager.BuildProjectBlocking(buildConfig, platform)) throw new Exception("Failed to build project"); // Add dependency assemblies @@ -289,6 +285,7 @@ namespace GodotTools.Export } } + [NotNull] private static string ExportDataDirectory(string[] features, string platform, bool isDebug, string outputDir) { string target = isDebug ? "release_debug" : "release"; @@ -343,18 +340,19 @@ namespace GodotTools.Export private static bool PlatformHasTemplateDir(string platform) { // OSX export templates are contained in a zip, so we place our custom template inside it and let Godot do the rest. - return !new[] { OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform); + return !new[] {OS.Platforms.OSX, OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform); } - private static string DeterminePlatformFromFeatures(IEnumerable<string> features) + private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) { foreach (var feature in features) { - if (OS.PlatformNameMap.TryGetValue(feature, out string platform)) - return platform; + if (OS.PlatformNameMap.TryGetValue(feature, out platform)) + return true; } - return null; + platform = null; + return false; } private static string GetBclProfileDir(string profile) @@ -391,7 +389,7 @@ namespace GodotTools.Export /// </summary> private static bool PlatformRequiresCustomBcl(string platform) { - if (new[] { OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5 }.Contains(platform)) + if (new[] {OS.Platforms.Android, OS.Platforms.iOS, OS.Platforms.HTML5}.Contains(platform)) return true; // The 'net_4_x' BCL is not compatible between Windows and the other platforms. @@ -432,7 +430,7 @@ namespace GodotTools.Export private static string DetermineDataDirNameForProject() { var appName = (string)ProjectSettings.GetSetting("application/config/name"); - string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); + string appNameSafe = appName.ToSafeDirName(); return $"data_{appNameSafe}"; } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index f330f9ed2c..3148458d7e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -38,13 +38,14 @@ namespace GodotTools public BottomPanel BottomPanel { get; private set; } - public PlaySettings? CurrentPlaySettings { get; set; } + public bool SkipBuildBeforePlaying { get; set; } = false; public static string ProjectAssemblyName { get { var projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + projectAssemblyName = projectAssemblyName.ToSafeDirName(); if (string.IsNullOrEmpty(projectAssemblyName)) projectAssemblyName = "UnnamedProject"; return projectAssemblyName; @@ -175,36 +176,6 @@ namespace GodotTools // Once shown a first time, it can be seen again via the Mono menu - it doesn't have to be exclusive from that time on. aboutDialog.Exclusive = false; } - - var fileSystemDock = GetEditorInterface().GetFileSystemDock(); - - fileSystemDock.FilesMoved += (file, newFile) => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - { - ProjectUtils.RenameItemInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file), ProjectSettings.GlobalizePath(newFile)); - } - }; - - fileSystemDock.FileRemoved += file => - { - if (Path.GetExtension(file) == Internal.CSharpLanguageExtension) - ProjectUtils.RemoveItemFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(file)); - }; - - fileSystemDock.FolderMoved += (oldFolder, newFolder) => - { - ProjectUtils.RenameItemsToNewFolderInProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder), ProjectSettings.GlobalizePath(newFolder)); - }; - - fileSystemDock.FolderRemoved += oldFolder => - { - ProjectUtils.RemoveItemsInFolderFromProjectChecked(GodotSharpDirs.ProjectCsProjPath, "Compile", - ProjectSettings.GlobalizePath(oldFolder)); - }; } } @@ -389,6 +360,37 @@ namespace GodotTools return BuildManager.EditorBuildCallback(); } + private void ApplyNecessaryChangesToSolution() + { + try + { + // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease + DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); + + var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) + ?? throw new Exception("Cannot open C# project"); + + // NOTE: The order in which changes are made to the project is important + + // Migrate to MSBuild project Sdks style if using the old style + ProjectUtils.MigrateToProjectSdksStyle(msbuildProject, ProjectAssemblyName); + + ProjectUtils.EnsureGodotSdkIsUpToDate(msbuildProject); + + if (msbuildProject.HasUnsavedChanges) + { + // Save a copy of the project before replacing it + FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); + + msbuildProject.Save(); + } + } + catch (Exception e) + { + GD.PushError(e.ToString()); + } + } + public override void EnablePlugin() { base.EnablePlugin(); @@ -468,42 +470,7 @@ namespace GodotTools if (File.Exists(GodotSharpDirs.ProjectSlnPath) && File.Exists(GodotSharpDirs.ProjectCsProjPath)) { - try - { - // Migrate solution from old configuration names to: Debug, ExportDebug and ExportRelease - DotNetSolution.MigrateFromOldConfigNames(GodotSharpDirs.ProjectSlnPath); - - var msbuildProject = ProjectUtils.Open(GodotSharpDirs.ProjectCsProjPath) - ?? throw new Exception("Cannot open C# project"); - - // NOTE: The order in which changes are made to the project is important - - // Migrate csproj from old configuration names to: Debug, ExportDebug and ExportRelease - ProjectUtils.MigrateFromOldConfigNames(msbuildProject); - - // Apply the other fixes only after configurations have been migrated - - // Make sure the existing project has the ProjectTypeGuids property (for VisualStudio) - ProjectUtils.EnsureHasProjectTypeGuids(msbuildProject); - - // Make sure the existing project has Api assembly references configured correctly - ProjectUtils.FixApiHintPath(msbuildProject); - - // Make sure the existing project references the Microsoft.NETFramework.ReferenceAssemblies nuget package - ProjectUtils.EnsureHasNugetNetFrameworkRefAssemblies(msbuildProject); - - if (msbuildProject.HasUnsavedChanges) - { - // Save a copy of the project before replacing it - FileUtils.SaveBackupCopy(GodotSharpDirs.ProjectCsProjPath); - - msbuildProject.Save(); - } - } - catch (Exception e) - { - GD.PushError(e.ToString()); - } + ApplyNecessaryChangesToSolution(); } else { diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 17f3339560..eb34a2d0f7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -330,9 +330,10 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings(); + // TODO: Add BuildBeforePlaying flag to PlayRequest + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; }); return Task.FromResult<Response>(new PlayResponse()); } @@ -341,10 +342,22 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = - new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); + // Tell the build callback whether the editor already built the solution or not + GodotSharpEditor.Instance.SkipBuildBeforePlaying = !(request.BuildBeforePlaying ?? true); + + // Pass the debugger agent settings to the player via an environment variables + // TODO: It would be better if this was an argument in EditorRunPlay instead + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", + "--debugger-agent=transport=dt_socket" + + $",address={request.DebuggerHost}:{request.DebuggerPort}" + + ",server=n"); + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; + + // Restore normal settings + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", ""); + GodotSharpEditor.Instance.SkipBuildBeforePlaying = false; }); return Task.FromResult<Response>(new DebugPlayResponse()); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs index 569f27649f..c72a84c513 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/ScriptClassParser.cs @@ -15,6 +15,10 @@ namespace GodotTools.Internals public bool Nested { get; } public long BaseCount { get; } + public string SearchName => Nested ? + Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) : + Name; + public ClassDecl(string name, string @namespace, bool nested, long baseCount) { Name = name; diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 79e4b7c794..a17c371117 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -45,7 +45,6 @@ #include "../mono_gd/gd_mono_marshal.h" #include "../utils/path_utils.h" #include "../utils/string_utils.h" -#include "csharp_project.h" #define CS_INDENT " " // 4 whitespaces diff --git a/modules/mono/editor/csharp_project.h b/modules/mono/editor/csharp_project.h deleted file mode 100644 index 515b8d3d62..0000000000 --- a/modules/mono/editor/csharp_project.h +++ /dev/null @@ -1,42 +0,0 @@ -/*************************************************************************/ -/* csharp_project.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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. */ -/*************************************************************************/ - -#ifndef CSHARP_PROJECT_H -#define CSHARP_PROJECT_H - -#include "core/ustring.h" - -namespace CSharpProject { - -void add_item(const String &p_project_path, const String &p_item_type, const String &p_include); - -} // namespace CSharpProject - -#endif // CSHARP_PROJECT_H diff --git a/modules/mono/editor/script_class_parser.cpp b/modules/mono/editor/script_class_parser.cpp index 012ccd5339..430c82953e 100644 --- a/modules/mono/editor/script_class_parser.cpp +++ b/modules/mono/editor/script_class_parser.cpp @@ -235,7 +235,7 @@ ScriptClassParser::Token ScriptClassParser::get_token() { if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) { //a number const CharType *rptr; - double number = String::to_double(&code[idx], &rptr); + double number = String::to_float(&code[idx], &rptr); idx += (rptr - &code[idx]); value = number; return TK_NUMBER; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs index 6a4f785551..3aecce50f5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/AABB.cs @@ -14,6 +14,10 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// Axis-Aligned Bounding Box. AABB consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct AABB : IEquatable<AABB> @@ -21,24 +25,55 @@ namespace Godot private Vector3 _position; private Vector3 _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector3 Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector3 Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector3 End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// Returns an AABB with equivalent position and size, modified so that + /// the most-negative corner is the origin and the size is positive. + /// </summary> + /// <returns>The modified AABB.</returns> + public AABB Abs() + { + Vector3 end = End; + Vector3 topLeft = new Vector3(Mathf.Min(_position.x, end.x), Mathf.Min(_position.y, end.y), Mathf.Min(_position.z, end.z)); + return new AABB(topLeft, _size.Abs()); + } + + /// <summary> + /// Returns true if this AABB completely encloses another one. + /// </summary> + /// <param name="with">The other AABB that may be enclosed.</param> + /// <returns>A bool for whether or not this AABB encloses `b`.</returns> public bool Encloses(AABB with) { Vector3 src_min = _position; @@ -54,33 +89,59 @@ namespace Godot src_max.z > dst_max.z; } + /// <summary> + /// Returns this AABB expanded to include a given point. + /// </summary> + /// <param name="point">The point to include.</param> + /// <returns>The expanded AABB.</returns> public AABB Expand(Vector3 point) { Vector3 begin = _position; Vector3 end = _position + _size; if (point.x < begin.x) + { begin.x = point.x; + } if (point.y < begin.y) + { begin.y = point.y; + } if (point.z < begin.z) + { begin.z = point.z; + } if (point.x > end.x) + { end.x = point.x; + } if (point.y > end.y) + { end.y = point.y; + } if (point.z > end.z) + { end.z = point.z; + } return new AABB(begin, end - begin); } + /// <summary> + /// Returns the area of the AABB. + /// </summary> + /// <returns>The area.</returns> public real_t GetArea() { return _size.x * _size.y * _size.z; } + /// <summary> + /// Gets the position of one of the 8 endpoints of the AABB. + /// </summary> + /// <param name="idx">Which endpoint to get.</param> + /// <returns>An endpoint of the AABB.</returns> public Vector3 GetEndpoint(int idx) { switch (idx) @@ -106,6 +167,10 @@ namespace Godot } } + /// <summary> + /// Returns the normalized longest axis of the AABB. + /// </summary> + /// <returns>A vector representing the normalized longest axis of the AABB.</returns> public Vector3 GetLongestAxis() { var axis = new Vector3(1f, 0f, 0f); @@ -125,6 +190,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the <see cref="Vector3.Axis"/> index of the longest axis of the AABB. + /// </summary> + /// <returns>A <see cref="Vector3.Axis"/> index for which axis is longest.</returns> public Vector3.Axis GetLongestAxisIndex() { var axis = Vector3.Axis.X; @@ -144,6 +213,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the scalar length of the longest axis of the AABB. + /// </summary> + /// <returns>The scalar length of the longest axis of the AABB.</returns> public real_t GetLongestAxisSize() { real_t max_size = _size.x; @@ -157,6 +230,10 @@ namespace Godot return max_size; } + /// <summary> + /// Returns the normalized shortest axis of the AABB. + /// </summary> + /// <returns>A vector representing the normalized shortest axis of the AABB.</returns> public Vector3 GetShortestAxis() { var axis = new Vector3(1f, 0f, 0f); @@ -176,6 +253,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the <see cref="Vector3.Axis"/> index of the shortest axis of the AABB. + /// </summary> + /// <returns>A <see cref="Vector3.Axis"/> index for which axis is shortest.</returns> public Vector3.Axis GetShortestAxisIndex() { var axis = Vector3.Axis.X; @@ -195,6 +276,10 @@ namespace Godot return axis; } + /// <summary> + /// Returns the scalar length of the shortest axis of the AABB. + /// </summary> + /// <returns>The scalar length of the shortest axis of the AABB.</returns> public real_t GetShortestAxisSize() { real_t max_size = _size.x; @@ -208,6 +293,12 @@ namespace Godot return max_size; } + /// <summary> + /// Returns the support point in a given direction. + /// This is useful for collision detection algorithms. + /// </summary> + /// <param name="dir">The direction to find support for.</param> + /// <returns>A vector representing the support.</returns> public Vector3 GetSupport(Vector3 dir) { Vector3 half_extents = _size * 0.5f; @@ -219,6 +310,11 @@ namespace Godot dir.z > 0f ? -half_extents.z : half_extents.z); } + /// <summary> + /// Returns a copy of the AABB grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown AABB.</returns> public AABB Grow(real_t by) { var res = this; @@ -233,16 +329,29 @@ namespace Godot return res; } + /// <summary> + /// Returns true if the AABB is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the AABB has area.</returns> public bool HasNoArea() { return _size.x <= 0f || _size.y <= 0f || _size.z <= 0f; } + /// <summary> + /// Returns true if the AABB has no surface (no size), or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the AABB has area.</returns> public bool HasNoSurface() { return _size.x <= 0f && _size.y <= 0f && _size.z <= 0f; } + /// <summary> + /// Returns true if the AABB contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the AABB contains `point`.</returns> public bool HasPoint(Vector3 point) { if (point.x < _position.x) @@ -261,6 +370,11 @@ namespace Godot return true; } + /// <summary> + /// Returns the intersection of this AABB and `b`. + /// </summary> + /// <param name="with">The other AABB.</param> + /// <returns>The clipped AABB.</returns> public AABB Intersection(AABB with) { Vector3 src_min = _position; @@ -297,24 +411,57 @@ namespace Godot return new AABB(min, max - min); } - public bool Intersects(AABB with) + /// <summary> + /// Returns true if the AABB overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="with">The other AABB to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(AABB with, bool includeBorders = false) { - if (_position.x >= with._position.x + with._size.x) - return false; - if (_position.x + _size.x <= with._position.x) - return false; - if (_position.y >= with._position.y + with._size.y) - return false; - if (_position.y + _size.y <= with._position.y) - return false; - if (_position.z >= with._position.z + with._size.z) - return false; - if (_position.z + _size.z <= with._position.z) - return false; + if (includeBorders) + { + if (_position.x > with._position.x + with._size.x) + return false; + if (_position.x + _size.x < with._position.x) + return false; + if (_position.y > with._position.y + with._size.y) + return false; + if (_position.y + _size.y < with._position.y) + return false; + if (_position.z > with._position.z + with._size.z) + return false; + if (_position.z + _size.z < with._position.z) + return false; + } + else + { + if (_position.x >= with._position.x + with._size.x) + return false; + if (_position.x + _size.x <= with._position.x) + return false; + if (_position.y >= with._position.y + with._size.y) + return false; + if (_position.y + _size.y <= with._position.y) + return false; + if (_position.z >= with._position.z + with._size.z) + return false; + if (_position.z + _size.z <= with._position.z) + return false; + } return true; } + /// <summary> + /// Returns true if the AABB is on both sides of `plane`. + /// </summary> + /// <param name="plane">The plane to check for intersection.</param> + /// <returns>A bool for whether or not the AABB intersects the plane.</returns> public bool IntersectsPlane(Plane plane) { Vector3[] points = @@ -335,14 +482,24 @@ namespace Godot for (int i = 0; i < 8; i++) { if (plane.DistanceTo(points[i]) > 0) + { over = true; + } else + { under = true; + } } return under && over; } + /// <summary> + /// Returns true if the AABB intersects the line segment between `from` and `to`. + /// </summary> + /// <param name="from">The start of the line segment.</param> + /// <param name="to">The end of the line segment.</param> + /// <returns>A bool for whether or not the AABB intersects the line segment.</returns> public bool IntersectsSegment(Vector3 from, Vector3 to) { real_t min = 0f; @@ -359,7 +516,9 @@ namespace Godot if (segFrom < segTo) { if (segFrom > boxEnd || segTo < boxBegin) + { return false; + } real_t length = segTo - segFrom; cmin = segFrom < boxBegin ? (boxBegin - segFrom) / length : 0f; @@ -368,7 +527,9 @@ namespace Godot else { if (segTo > boxEnd || segFrom < boxBegin) + { return false; + } real_t length = segTo - segFrom; cmin = segFrom > boxEnd ? (boxEnd - segFrom) / length : 0f; @@ -381,14 +542,23 @@ namespace Godot } if (cmax < max) + { max = cmax; + } if (max < min) + { return false; + } } return true; } + /// <summary> + /// Returns a larger AABB that contains this AABB and `b`. + /// </summary> + /// <param name="with">The other AABB.</param> + /// <returns>The merged AABB.</returns> public AABB Merge(AABB with) { Vector3 beg1 = _position; @@ -411,22 +581,52 @@ namespace Godot return new AABB(min, max - min); } - // Constructors + /// <summary> + /// Constructs an AABB from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size, typically positive.</param> public AABB(Vector3 position, Vector3 size) { _position = position; _size = size; } + + /// <summary> + /// Constructs an AABB from a position, width, height, and depth. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width, typically positive.</param> + /// <param name="height">The height, typically positive.</param> + /// <param name="depth">The depth, typically positive.</param> public AABB(Vector3 position, real_t width, real_t height, real_t depth) { _position = position; _size = new Vector3(width, height, depth); } + + /// <summary> + /// Constructs an AABB from x, y, z, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="z">The position's Z coordinate.</param> + /// <param name="size">The size, typically positive.</param> public AABB(real_t x, real_t y, real_t z, Vector3 size) { _position = new Vector3(x, y, z); _size = size; } + + /// <summary> + /// Constructs an AABB from x, y, z, width, height, and depth. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="z">The position's Z coordinate.</param> + /// <param name="width">The width, typically positive.</param> + /// <param name="height">The height, typically positive.</param> + /// <param name="depth">The depth, typically positive.</param> public AABB(real_t x, real_t y, real_t z, real_t width, real_t height, real_t depth) { _position = new Vector3(x, y, z); @@ -458,6 +658,12 @@ namespace Godot return _position == other._position && _size == other._size; } + /// <summary> + /// Returns true if this AABB and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other AABB to compare.</param> + /// <returns>Whether or not the AABBs are approximately equal.</returns> public bool IsEqualApprox(AABB other) { return _position.IsEqualApprox(other._position) && _size.IsEqualApprox(other._size); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs index baf470a0cc..5aba31c622 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs @@ -8,6 +8,20 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 3×3 matrix used for 3D rotation and scale. + /// Almost always used as an orthogonal basis for a Transform. + /// + /// Contains 3 vector fields X, Y and Z as its columns, which are typically + /// interpreted as the local basis vectors of a 3D transformation. For such use, + /// it is composed of a scaling and a rotation matrix, in that order (M = R.S). + /// + /// Can also be accessed as array of 3D vectors. These vectors are normally + /// orthogonal to each other, but are not necessarily normalized (due to scaling). + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Basis : IEquatable<Basis> @@ -15,9 +29,9 @@ namespace Godot // NOTE: x, y and z are public-only. Use Column0, Column1 and Column2 internally. /// <summary> - /// Returns the basis matrix’s x vector. - /// This is equivalent to <see cref="Column0"/>. + /// The basis matrix's X vector (column 0). /// </summary> + /// <value>Equivalent to <see cref="Column0"/> and array index `[0]`.</value> public Vector3 x { get => Column0; @@ -25,9 +39,9 @@ namespace Godot } /// <summary> - /// Returns the basis matrix’s y vector. - /// This is equivalent to <see cref="Column1"/>. + /// The basis matrix's Y vector (column 1). /// </summary> + /// <value>Equivalent to <see cref="Column1"/> and array index `[1]`.</value> public Vector3 y { get => Column1; @@ -35,19 +49,40 @@ namespace Godot } /// <summary> - /// Returns the basis matrix’s z vector. - /// This is equivalent to <see cref="Column2"/>. + /// The basis matrix's Z vector (column 2). /// </summary> + /// <value>Equivalent to <see cref="Column2"/> and array index `[2]`.</value> public Vector3 z { get => Column2; set => Column2 = value; } + /// <summary> + /// Row 0 of the basis matrix. Shows which vectors contribute + /// to the X direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row0; + + /// <summary> + /// Row 1 of the basis matrix. Shows which vectors contribute + /// to the Y direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row1; + + /// <summary> + /// Row 2 of the basis matrix. Shows which vectors contribute + /// to the Z direction. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> public Vector3 Row2; + /// <summary> + /// Column 0 of the basis matrix (the X vector). + /// </summary> + /// <value>Equivalent to <see cref="x"/> and array index `[0]`.</value> public Vector3 Column0 { get => new Vector3(Row0.x, Row1.x, Row2.x); @@ -58,6 +93,11 @@ namespace Godot this.Row2.x = value.z; } } + + /// <summary> + /// Column 1 of the basis matrix (the Y vector). + /// </summary> + /// <value>Equivalent to <see cref="y"/> and array index `[1]`.</value> public Vector3 Column1 { get => new Vector3(Row0.y, Row1.y, Row2.y); @@ -68,6 +108,11 @@ namespace Godot this.Row2.y = value.z; } } + + /// <summary> + /// Column 2 of the basis matrix (the Z vector). + /// </summary> + /// <value>Equivalent to <see cref="z"/> and array index `[2]`.</value> public Vector3 Column2 { get => new Vector3(Row0.z, Row1.z, Row2.z); @@ -79,6 +124,10 @@ namespace Godot } } + /// <summary> + /// The scale of this basis. + /// </summary> + /// <value>Equivalent to the lengths of each column vector, but negative if the determinant is negative.</value> public Vector3 Scale { get @@ -86,11 +135,18 @@ namespace Godot real_t detSign = Mathf.Sign(Determinant()); return detSign * new Vector3 ( - new Vector3(this.Row0[0], this.Row1[0], this.Row2[0]).Length(), - new Vector3(this.Row0[1], this.Row1[1], this.Row2[1]).Length(), - new Vector3(this.Row0[2], this.Row1[2], this.Row2[2]).Length() + Column0.Length(), + Column1.Length(), + Column2.Length() ); } + set + { + value /= Scale; // Value becomes what's called "delta_scale" in core. + Column0 *= value.x; + Column1 *= value.y; + Column2 *= value.z; + } } /// <summary> @@ -157,8 +213,9 @@ namespace Godot real_t det = orthonormalizedBasis.Determinant(); if (det < 0) { - // Ensure that the determinant is 1, such that result is a proper rotation matrix which can be represented by Euler angles. - orthonormalizedBasis = orthonormalizedBasis.Scaled(Vector3.NegOne); + // Ensure that the determinant is 1, such that result is a proper + // rotation matrix which can be represented by Euler angles. + orthonormalizedBasis = orthonormalizedBasis.Scaled(-Vector3.One); } return orthonormalizedBasis.Quat(); @@ -182,6 +239,15 @@ namespace Godot Row2 = new Vector3(0, 0, diagonal.z); } + /// <summary> + /// Returns the determinant of the basis matrix. If the basis is + /// uniformly scaled, its determinant is the square of the scale. + /// + /// A negative determinant means the basis has a negative scale. + /// A zero determinant means the basis isn't invertible, + /// and is usually considered invalid. + /// </summary> + /// <returns>The determinant of the basis matrix.</returns> public real_t Determinant() { real_t cofac00 = Row1[1] * Row2[2] - Row1[2] * Row2[1]; @@ -191,6 +257,16 @@ namespace Godot return Row0[0] * cofac00 + Row0[1] * cofac10 + Row0[2] * cofac20; } + /// <summary> + /// Returns the basis's rotation in the form of Euler angles + /// (in the YXZ convention: when *decomposing*, first Z, then X, and Y last). + /// The returned vector contains the rotation angles in + /// the format (X angle, Y angle, Z angle). + /// + /// Consider using the <see cref="Basis.Quat()"/> method instead, which + /// returns a <see cref="Godot.Quat"/> quaternion instead of Euler angles. + /// </summary> + /// <returns>A Vector3 representing the basis rotation in Euler angles.</returns> public Vector3 GetEuler() { Basis m = Orthonormalized(); @@ -223,6 +299,12 @@ namespace Godot return euler; } + /// <summary> + /// Get rows by index. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> + /// <param name="index">Which row.</param> + /// <returns>One of `Row0`, `Row1`, or `Row2`.</returns> public Vector3 GetRow(int index) { switch (index) @@ -238,6 +320,12 @@ namespace Godot } } + /// <summary> + /// Sets rows by index. Rows are not very useful for user code, + /// but are more efficient for some internal calculations. + /// </summary> + /// <param name="index">Which row.</param> + /// <param name="value">The vector to set the row to.</param> public void SetRow(int index, Vector3 value) { switch (index) @@ -256,16 +344,16 @@ namespace Godot } } - public Vector3 GetColumn(int index) - { - return this[index]; - } - - public void SetColumn(int index, Vector3 value) - { - this[index] = value; - } - + /// <summary> + /// This function considers a discretization of rotations into + /// 24 points on unit sphere, lying along the vectors (x, y, z) with + /// each component being either -1, 0, or 1, and returns the index + /// of the point best representing the orientation of the object. + /// It is mainly used by the <see cref="GridMap"/> editor. + /// + /// For further details, refer to the Godot source code. + /// </summary> + /// <returns>The orthogonal index.</returns> public int GetOrthogonalIndex() { var orth = this; @@ -279,11 +367,17 @@ namespace Godot real_t v = row[j]; if (v > 0.5f) + { v = 1.0f; + } else if (v < -0.5f) + { v = -1.0f; + } else + { v = 0f; + } row[j] = v; @@ -294,12 +388,18 @@ namespace Godot for (int i = 0; i < 24; i++) { if (orth == _orthoBases[i]) + { return i; + } } return 0; } + /// <summary> + /// Returns the inverse of the matrix. + /// </summary> + /// <returns>The inverse matrix.</returns> public Basis Inverse() { real_t cofac00 = Row1[1] * Row2[2] - Row1[2] * Row2[1]; @@ -309,7 +409,9 @@ namespace Godot real_t det = Row0[0] * cofac00 + Row0[1] * cofac10 + Row0[2] * cofac20; if (det == 0) + { throw new InvalidOperationException("Matrix determinant is zero and cannot be inverted."); + } real_t detInv = 1.0f / det; @@ -328,11 +430,17 @@ namespace Godot ); } + /// <summary> + /// Returns the orthonormalized version of the basis matrix (useful to + /// call occasionally to avoid rounding errors for orthogonal matrices). + /// This performs a Gram-Schmidt orthonormalization on the basis of the matrix. + /// </summary> + /// <returns>An orthonormalized basis matrix.</returns> public Basis Orthonormalized() { - Vector3 column0 = GetColumn(0); - Vector3 column1 = GetColumn(1); - Vector3 column2 = GetColumn(2); + Vector3 column0 = this[0]; + Vector3 column1 = this[1]; + Vector3 column2 = this[2]; column0.Normalize(); column1 = column1 - column0 * column0.Dot(column1); @@ -343,48 +451,86 @@ namespace Godot return new Basis(column0, column1, column2); } + /// <summary> + /// Introduce an additional rotation around the given `axis` + /// by `phi` (in radians). The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated basis matrix.</returns> public Basis Rotated(Vector3 axis, real_t phi) { return new Basis(axis, phi) * this; } + /// <summary> + /// Introduce an additional scaling specified by the given 3D scaling factor. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled basis matrix.</returns> public Basis Scaled(Vector3 scale) { - var b = this; + Basis b = this; b.Row0 *= scale.x; b.Row1 *= scale.y; b.Row2 *= scale.z; return b; } - public Basis Slerp(Basis target, real_t t) + /// <summary> + /// Assuming that the matrix is a proper rotation matrix, slerp performs + /// a spherical-linear interpolation with another rotation matrix. + /// </summary> + /// <param name="target">The destination basis for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting basis matrix of the interpolation.</returns> + public Basis Slerp(Basis target, real_t weight) { - var from = new Quat(this); - var to = new Quat(target); + Quat from = new Quat(this); + Quat to = new Quat(target); - var b = new Basis(from.Slerp(to, t)); - b.Row0 *= Mathf.Lerp(Row0.Length(), target.Row0.Length(), t); - b.Row1 *= Mathf.Lerp(Row1.Length(), target.Row1.Length(), t); - b.Row2 *= Mathf.Lerp(Row2.Length(), target.Row2.Length(), t); + Basis b = new Basis(from.Slerp(to, weight)); + b.Row0 *= Mathf.Lerp(Row0.Length(), target.Row0.Length(), weight); + b.Row1 *= Mathf.Lerp(Row1.Length(), target.Row1.Length(), weight); + b.Row2 *= Mathf.Lerp(Row2.Length(), target.Row2.Length(), weight); return b; } + /// <summary> + /// Transposed dot product with the X axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdotx(Vector3 with) { return this.Row0[0] * with[0] + this.Row1[0] * with[1] + this.Row2[0] * with[2]; } + /// <summary> + /// Transposed dot product with the Y axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdoty(Vector3 with) { return this.Row0[1] * with[0] + this.Row1[1] * with[1] + this.Row2[1] * with[2]; } + /// <summary> + /// Transposed dot product with the Z axis of the matrix. + /// </summary> + /// <param name="with">A vector to calculate the dot product with.</param> + /// <returns>The resulting dot product.</returns> public real_t Tdotz(Vector3 with) { return this.Row0[2] * with[0] + this.Row1[2] * with[1] + this.Row2[2] * with[2]; } + /// <summary> + /// Returns the transposed version of the basis matrix. + /// </summary> + /// <returns>The transposed basis matrix.</returns> public Basis Transposed() { var tr = this; @@ -404,6 +550,11 @@ namespace Godot return tr; } + /// <summary> + /// Returns a vector transformed (multiplied) by the basis matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { return new Vector3 @@ -414,6 +565,14 @@ namespace Godot ); } + /// <summary> + /// Returns a vector transformed (multiplied) by the transposed basis matrix. + /// + /// Note: This results in a multiplication by the inverse of the + /// basis matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector3 XformInv(Vector3 v) { return new Vector3 @@ -424,6 +583,12 @@ namespace Godot ); } + /// <summary> + /// Returns the basis's rotation in the form of a quaternion. + /// See <see cref="GetEuler()"/> if you need Euler angles, but keep in + /// mind that quaternions should generally be preferred to Euler angles. + /// </summary> + /// <returns>A <see cref="Godot.Quat"/> representing the basis's rotation.</returns> public Quat Quat() { real_t trace = Row0[0] + Row1[1] + Row2[2]; @@ -508,11 +673,33 @@ namespace Godot private static readonly Basis _flipY = new Basis(1, 0, 0, 0, -1, 0, 0, 0, 1); private static readonly Basis _flipZ = new Basis(1, 0, 0, 0, 1, 0, 0, 0, -1); + /// <summary> + /// The identity basis, with no rotation or scaling applied. + /// This is used as a replacement for `Basis()` in GDScript. + /// Do not use `new Basis()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Up, Vector3.Back)`.</value> public static Basis Identity { get { return _identity; } } + /// <summary> + /// The basis that will flip something along the X axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Left, Vector3.Up, Vector3.Back)`.</value> public static Basis FlipX { get { return _flipX; } } + /// <summary> + /// The basis that will flip something along the Y axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Down, Vector3.Back)`.</value> public static Basis FlipY { get { return _flipY; } } + /// <summary> + /// The basis that will flip something along the Z axis when used in a transformation. + /// </summary> + /// <value>Equivalent to `new Basis(Vector3.Right, Vector3.Up, Vector3.Forward)`.</value> public static Basis FlipZ { get { return _flipZ; } } + /// <summary> + /// Constructs a pure rotation basis matrix from the given quaternion. + /// </summary> + /// <param name="quat">The quaternion to create the basis from.</param> public Basis(Quat quat) { real_t s = 2.0f / quat.LengthSquared; @@ -535,26 +722,41 @@ namespace Godot Row2 = new Vector3(xz - wy, yz + wx, 1.0f - (xx + yy)); } - public Basis(Vector3 euler) + /// <summary> + /// Constructs a pure rotation basis matrix from the given Euler angles + /// (in the YXZ convention: when *composing*, first Y, then X, and Z last), + /// given in the vector format as (X angle, Y angle, Z angle). + /// + /// Consider using the <see cref="Basis(Quat)"/> constructor instead, which + /// uses a <see cref="Godot.Quat"/> quaternion instead of Euler angles. + /// </summary> + /// <param name="eulerYXZ">The Euler angles to create the basis from.</param> + public Basis(Vector3 eulerYXZ) { real_t c; real_t s; - c = Mathf.Cos(euler.x); - s = Mathf.Sin(euler.x); + c = Mathf.Cos(eulerYXZ.x); + s = Mathf.Sin(eulerYXZ.x); var xmat = new Basis(1, 0, 0, 0, c, -s, 0, s, c); - c = Mathf.Cos(euler.y); - s = Mathf.Sin(euler.y); + c = Mathf.Cos(eulerYXZ.y); + s = Mathf.Sin(eulerYXZ.y); var ymat = new Basis(c, 0, s, 0, 1, 0, -s, 0, c); - c = Mathf.Cos(euler.z); - s = Mathf.Sin(euler.z); + c = Mathf.Cos(eulerYXZ.z); + s = Mathf.Sin(eulerYXZ.z); var zmat = new Basis(c, -s, 0, s, c, 0, 0, 0, 1); this = ymat * xmat * zmat; } + /// <summary> + /// Constructs a pure rotation basis matrix, rotated around the given `axis` + /// by `phi` (in radians). The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> public Basis(Vector3 axis, real_t phi) { Vector3 axisSq = new Vector3(axis.x * axis.x, axis.y * axis.y, axis.z * axis.z); @@ -582,6 +784,12 @@ namespace Godot Row2.y = xyzt + zyxs; } + /// <summary> + /// Constructs a basis matrix from 3 axis vectors (matrix columns). + /// </summary> + /// <param name="column0">The X vector, or Column0.</param> + /// <param name="column1">The Y vector, or Column1.</param> + /// <param name="column2">The Z vector, or Column2.</param> public Basis(Vector3 column0, Vector3 column1, Vector3 column2) { Row0 = new Vector3(column0.x, column1.x, column2.x); @@ -637,6 +845,12 @@ namespace Godot return Row0.Equals(other.Row0) && Row1.Equals(other.Row1) && Row2.Equals(other.Row2); } + /// <summary> + /// Returns true if this basis and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other basis to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Basis other) { return Row0.IsEqualApprox(other.Row0) && Row1.IsEqualApprox(other.Row1) && Row2.IsEqualApprox(other.Row2); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs index 6030b72a44..d851abc6d3 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs @@ -3,15 +3,44 @@ using System.Runtime.InteropServices; namespace Godot { + /// <summary> + /// A color represented by red, green, blue, and alpha (RGBA) components. + /// The alpha component is often used for transparency. + /// Values are in floating-point and usually range from 0 to 1. + /// Some properties (such as CanvasItem.modulate) may accept values + /// greater than 1 (overbright or HDR colors). + /// + /// If you want to supply values in a range of 0 to 255, you should use + /// <see cref="Color8"/> and the `r8`/`g8`/`b8`/`a8` properties. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Color : IEquatable<Color> { + /// <summary> + /// The color's red component, typically on the range of 0 to 1. + /// </summary> public float r; + + /// <summary> + /// The color's green component, typically on the range of 0 to 1. + /// </summary> public float g; + + /// <summary> + /// The color's blue component, typically on the range of 0 to 1. + /// </summary> public float b; + + /// <summary> + /// The color's alpha (transparency) component, typically on the range of 0 to 1. + /// </summary> public float a; + /// <summary> + /// Wrapper for <see cref="r"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int r8 { get @@ -24,6 +53,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="g"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int g8 { get @@ -36,6 +69,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="b"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int b8 { get @@ -48,6 +85,10 @@ namespace Godot } } + /// <summary> + /// Wrapper for <see cref="a"/> that uses the range 0 to 255 instead of 0 to 1. + /// </summary> + /// <value>Getting is equivalent to multiplying by 255 and rounding. Setting is equivalent to dividing by 255.</value> public int a8 { get @@ -60,6 +101,10 @@ namespace Godot } } + /// <summary> + /// The HSV hue of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is a long process, refer to the source code for details. Setting uses <see cref="FromHsv"/>.</value> public float h { get @@ -70,21 +115,31 @@ namespace Godot float delta = max - min; if (delta == 0) + { return 0; + } float h; if (r == max) + { h = (g - b) / delta; // Between yellow & magenta + } else if (g == max) + { h = 2 + (b - r) / delta; // Between cyan & yellow + } else + { h = 4 + (r - g) / delta; // Between magenta & cyan + } h /= 6.0f; if (h < 0) + { h += 1.0f; + } return h; } @@ -94,6 +149,10 @@ namespace Godot } } + /// <summary> + /// The HSV saturation of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is equivalent to the ratio between the min and max RGB value. Setting uses <see cref="FromHsv"/>.</value> public float s { get @@ -103,7 +162,7 @@ namespace Godot float delta = max - min; - return max != 0 ? delta / max : 0; + return max == 0 ? 0 : delta / max; } set { @@ -111,6 +170,10 @@ namespace Godot } } + /// <summary> + /// The HSV value (brightness) of this color, on the range 0 to 1. + /// </summary> + /// <value>Getting is equivalent to using `Max()` on the RGB components. Setting uses <see cref="FromHsv"/>.</value> public float v { get @@ -123,25 +186,10 @@ namespace Godot } } - public static Color ColorN(string name, float alpha = 1f) - { - name = name.Replace(" ", String.Empty); - name = name.Replace("-", String.Empty); - name = name.Replace("_", String.Empty); - name = name.Replace("'", String.Empty); - name = name.Replace(".", String.Empty); - name = name.ToLower(); - - if (!Colors.namedColors.ContainsKey(name)) - { - throw new ArgumentOutOfRangeException($"Invalid Color Name: {name}"); - } - - Color color = Colors.namedColors[name]; - color.a = alpha; - return color; - } - + /// <summary> + /// Access color components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.r`, `[1]` is equivalent to `.g`, `[2]` is equivalent to `.b`, `[3]` is equivalent to `.a`.</value> public float this[int index] { get @@ -182,73 +230,13 @@ namespace Godot } } - public void ToHsv(out float hue, out float saturation, out float value) - { - float max = (float)Mathf.Max(r, Mathf.Max(g, b)); - float min = (float)Mathf.Min(r, Mathf.Min(g, b)); - - float delta = max - min; - - if (delta == 0) - { - hue = 0; - } - else - { - if (r == max) - hue = (g - b) / delta; // Between yellow & magenta - else if (g == max) - hue = 2 + (b - r) / delta; // Between cyan & yellow - else - hue = 4 + (r - g) / delta; // Between magenta & cyan - - hue /= 6.0f; - - if (hue < 0) - hue += 1.0f; - } - - saturation = max == 0 ? 0 : 1f - 1f * min / max; - value = max; - } - - public static Color FromHsv(float hue, float saturation, float value, float alpha = 1.0f) - { - if (saturation == 0) - { - // acp_hromatic (grey) - return new Color(value, value, value, alpha); - } - - int i; - float f, p, q, t; - - hue *= 6.0f; - hue %= 6f; - i = (int)hue; - - f = hue - i; - p = value * (1 - saturation); - q = value * (1 - saturation * f); - t = value * (1 - saturation * (1 - f)); - - switch (i) - { - case 0: // Red is the dominant color - return new Color(value, t, p, alpha); - case 1: // Green is the dominant color - return new Color(q, value, p, alpha); - case 2: - return new Color(p, value, t, alpha); - case 3: // Blue is the dominant color - return new Color(p, q, value, alpha); - case 4: - return new Color(t, p, value, alpha); - default: // (5) Red is the dominant color - return new Color(value, p, q, alpha); - } - } - + /// <summary> + /// Returns a new color resulting from blending this color over another. + /// If the color is opaque, the result is also opaque. + /// The second color may have a range of alpha values. + /// </summary> + /// <param name="over">The color to blend over.</param> + /// <returns>This color blended over `over`.</returns> public Color Blend(Color over) { Color res; @@ -268,6 +256,10 @@ namespace Godot return res; } + /// <summary> + /// Returns the most contrasting color. + /// </summary> + /// <returns>The most contrasting color</returns> public Color Contrasted() { return new Color( @@ -278,6 +270,12 @@ namespace Godot ); } + /// <summary> + /// Returns a new color resulting from making this color darker + /// by the specified ratio (on the range of 0 to 1). + /// </summary> + /// <param name="amount">The ratio to darken by.</param> + /// <returns>The darkened color.</returns> public Color Darkened(float amount) { Color res = this; @@ -287,6 +285,10 @@ namespace Godot return res; } + /// <summary> + /// Returns the inverted color: `(1 - r, 1 - g, 1 - b, a)`. + /// </summary> + /// <returns>The inverted color.</returns> public Color Inverted() { return new Color( @@ -297,6 +299,12 @@ namespace Godot ); } + /// <summary> + /// Returns a new color resulting from making this color lighter + /// by the specified ratio (on the range of 0 to 1). + /// </summary> + /// <param name="amount">The ratio to lighten by.</param> + /// <returns>The darkened color.</returns> public Color Lightened(float amount) { Color res = this; @@ -306,6 +314,13 @@ namespace Godot return res; } + /// <summary> + /// Returns the result of the linear interpolation between + /// this color and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination color for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting color of the interpolation.</returns> public Color Lerp(Color to, float weight) { return new Color @@ -317,6 +332,13 @@ namespace Godot ); } + /// <summary> + /// Returns the result of the linear interpolation between + /// this color and `to` by color amount `weight`. + /// </summary> + /// <param name="to">The destination color for interpolation.</param> + /// <param name="weight">A color with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting color of the interpolation.</returns> public Color Lerp(Color to, Color weight) { return new Color @@ -328,6 +350,12 @@ namespace Godot ); } + /// <summary> + /// Returns the color's 32-bit integer in ABGR format + /// (each byte represents a component of the ABGR profile). + /// ABGR is the reversed version of the default format. + /// </summary> + /// <returns>A uint representing this color in ABGR32 format.</returns> public uint ToAbgr32() { uint c = (byte)Math.Round(a * 255); @@ -341,6 +369,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in ABGR format + /// (each word represents a component of the ABGR profile). + /// ABGR is the reversed version of the default format. + /// </summary> + /// <returns>A ulong representing this color in ABGR64 format.</returns> public ulong ToAbgr64() { ulong c = (ushort)Math.Round(a * 65535); @@ -354,6 +388,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 32-bit integer in ARGB format + /// (each byte represents a component of the ARGB profile). + /// ARGB is more compatible with DirectX, but not used much in Godot. + /// </summary> + /// <returns>A uint representing this color in ARGB32 format.</returns> public uint ToArgb32() { uint c = (byte)Math.Round(a * 255); @@ -367,6 +407,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in ARGB format + /// (each word represents a component of the ARGB profile). + /// ARGB is more compatible with DirectX, but not used much in Godot. + /// </summary> + /// <returns>A ulong representing this color in ARGB64 format.</returns> public ulong ToArgb64() { ulong c = (ushort)Math.Round(a * 65535); @@ -380,6 +426,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 32-bit integer in RGBA format + /// (each byte represents a component of the RGBA profile). + /// RGBA is Godot's default and recommended format. + /// </summary> + /// <returns>A uint representing this color in RGBA32 format.</returns> public uint ToRgba32() { uint c = (byte)Math.Round(r * 255); @@ -393,6 +445,12 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's 64-bit integer in RGBA format + /// (each word represents a component of the RGBA profile). + /// RGBA is Godot's default and recommended format. + /// </summary> + /// <returns>A ulong representing this color in RGBA64 format.</returns> public ulong ToRgba64() { ulong c = (ushort)Math.Round(r * 65535); @@ -406,6 +464,11 @@ namespace Godot return c; } + /// <summary> + /// Returns the color's HTML hexadecimal color string in RGBA format. + /// </summary> + /// <param name="includeAlpha">Whether or not to include alpha. If false, the color is RGB instead of RGBA.</param> + /// <returns>A string for the HTML hexadecimal representation of this color.</returns> public string ToHtml(bool includeAlpha = true) { var txt = string.Empty; @@ -415,12 +478,20 @@ namespace Godot txt += ToHex32(b); if (includeAlpha) - txt = ToHex32(a) + txt; + { + txt += ToHex32(a); + } return txt; } - // Constructors + /// <summary> + /// Constructs a color from RGBA values on the range of 0 to 1. + /// </summary> + /// <param name="r">The color's red component, typically on the range of 0 to 1.</param> + /// <param name="g">The color's green component, typically on the range of 0 to 1.</param> + /// <param name="b">The color's blue component, typically on the range of 0 to 1.</param> + /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> public Color(float r, float g, float b, float a = 1.0f) { this.r = r; @@ -429,6 +500,11 @@ namespace Godot this.a = a; } + /// <summary> + /// Constructs a color from an existing color and an alpha value. + /// </summary> + /// <param name="c">The color to construct from. Only its RGB values are used.</param> + /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> public Color(Color c, float a = 1.0f) { r = c.r; @@ -437,6 +513,11 @@ namespace Godot this.a = a; } + /// <summary> + /// Constructs a color from a 32-bit integer + /// (each byte represents a component of the RGBA profile). + /// </summary> + /// <param name="rgba">The uint representing the color.</param> public Color(uint rgba) { a = (rgba & 0xFF) / 255.0f; @@ -448,6 +529,11 @@ namespace Godot r = (rgba & 0xFF) / 255.0f; } + /// <summary> + /// Constructs a color from a 64-bit integer + /// (each word represents a component of the RGBA profile). + /// </summary> + /// <param name="rgba">The ulong representing the color.</param> public Color(ulong rgba) { a = (rgba & 0xFFFF) / 65535.0f; @@ -459,6 +545,212 @@ namespace Godot r = (rgba & 0xFFFF) / 65535.0f; } + /// <summary> + /// Constructs a color from the HTML hexadecimal color string in RGBA format. + /// </summary> + /// <param name="rgba">A string for the HTML hexadecimal representation of this color.</param> + public Color(string rgba) + { + if (rgba.Length == 0) + { + r = 0f; + g = 0f; + b = 0f; + a = 1.0f; + return; + } + + if (rgba[0] == '#') + { + rgba = rgba.Substring(1); + } + + bool alpha; + + if (rgba.Length == 8) + { + alpha = true; + } + else if (rgba.Length == 6) + { + alpha = false; + } + else + { + throw new ArgumentOutOfRangeException("Invalid color code. Length is " + rgba.Length + " but a length of 6 or 8 is expected: " + rgba); + } + + if (alpha) + { + a = ParseCol8(rgba, 6) / 255f; + + if (a < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Alpha part is not valid hexadecimal: " + rgba); + } + } + else + { + a = 1.0f; + } + + int from = alpha ? 2 : 0; + + r = ParseCol8(rgba, 0) / 255f; + + if (r < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Red part is not valid hexadecimal: " + rgba); + } + + g = ParseCol8(rgba, 2) / 255f; + + if (g < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Green part is not valid hexadecimal: " + rgba); + } + + b = ParseCol8(rgba, 4) / 255f; + + if (b < 0) + { + throw new ArgumentOutOfRangeException("Invalid color code. Blue part is not valid hexadecimal: " + rgba); + } + } + + /// <summary> + /// Returns a color constructed from integer red, green, blue, and alpha channels. + /// Each channel should have 8 bits of information ranging from 0 to 255. + /// </summary> + /// <param name="r8">The red component represented on the range of 0 to 255.</param> + /// <param name="g8">The green component represented on the range of 0 to 255.</param> + /// <param name="b8">The blue component represented on the range of 0 to 255.</param> + /// <param name="a8">The alpha (transparency) component represented on the range of 0 to 255.</param> + /// <returns>The constructed color.</returns> + public static Color Color8(byte r8, byte g8, byte b8, byte a8 = 255) + { + return new Color(r8 / 255f, g8 / 255f, b8 / 255f, a8 / 255f); + } + + /// <summary> + /// Returns a color according to the standardized name, with the + /// specified alpha value. Supported color names are the same as + /// the constants defined in <see cref="Colors"/>. + /// </summary> + /// <param name="name">The name of the color.</param> + /// <param name="alpha">The alpha (transparency) component represented on the range of 0 to 1. Default: 1.</param> + /// <returns>The constructed color.</returns> + public static Color ColorN(string name, float alpha = 1f) + { + name = name.Replace(" ", String.Empty); + name = name.Replace("-", String.Empty); + name = name.Replace("_", String.Empty); + name = name.Replace("'", String.Empty); + name = name.Replace(".", String.Empty); + name = name.ToLower(); + + if (!Colors.namedColors.ContainsKey(name)) + { + throw new ArgumentOutOfRangeException($"Invalid Color Name: {name}"); + } + + Color color = Colors.namedColors[name]; + color.a = alpha; + return color; + } + + /// <summary> + /// Constructs a color from an HSV profile, with values on the + /// range of 0 to 1. This is equivalent to using each of + /// the `h`/`s`/`v` properties, but much more efficient. + /// </summary> + /// <param name="hue">The HSV hue, typically on the range of 0 to 1.</param> + /// <param name="saturation">The HSV saturation, typically on the range of 0 to 1.</param> + /// <param name="value">The HSV value (brightness), typically on the range of 0 to 1.</param> + /// <param name="alpha">The alpha (transparency) value, typically on the range of 0 to 1.</param> + /// <returns>The constructed color.</returns> + public static Color FromHsv(float hue, float saturation, float value, float alpha = 1.0f) + { + if (saturation == 0) + { + // acp_hromatic (grey) + return new Color(value, value, value, alpha); + } + + int i; + float f, p, q, t; + + hue *= 6.0f; + hue %= 6f; + i = (int)hue; + + f = hue - i; + p = value * (1 - saturation); + q = value * (1 - saturation * f); + t = value * (1 - saturation * (1 - f)); + + switch (i) + { + case 0: // Red is the dominant color + return new Color(value, t, p, alpha); + case 1: // Green is the dominant color + return new Color(q, value, p, alpha); + case 2: + return new Color(p, value, t, alpha); + case 3: // Blue is the dominant color + return new Color(p, q, value, alpha); + case 4: + return new Color(t, p, value, alpha); + default: // (5) Red is the dominant color + return new Color(value, p, q, alpha); + } + } + + /// <summary> + /// Converts a color to HSV values. This is equivalent to using each of + /// the `h`/`s`/`v` properties, but much more efficient. + /// </summary> + /// <param name="hue">Output parameter for the HSV hue.</param> + /// <param name="saturation">Output parameter for the HSV saturation.</param> + /// <param name="value">Output parameter for the HSV value.</param> + public void ToHsv(out float hue, out float saturation, out float value) + { + float max = (float)Mathf.Max(r, Mathf.Max(g, b)); + float min = (float)Mathf.Min(r, Mathf.Min(g, b)); + + float delta = max - min; + + if (delta == 0) + { + hue = 0; + } + else + { + if (r == max) + { + hue = (g - b) / delta; // Between yellow & magenta + } + else if (g == max) + { + hue = 2 + (b - r) / delta; // Between cyan & yellow + } + else + { + hue = 4 + (r - g) / delta; // Between magenta & cyan + } + + hue /= 6.0f; + + if (hue < 0) + { + hue += 1.0f; + } + } + + saturation = max == 0 ? 0 : 1f - 1f * min / max; + value = max; + } + private static int ParseCol8(string str, int ofs) { int ig = 0; @@ -488,9 +780,13 @@ namespace Godot } if (i == 0) + { ig += v * 16; + } else + { ig += v; + } } return ig; @@ -508,9 +804,13 @@ namespace Godot int lv = v & 0xF; if (lv < 10) + { c = (char)('0' + lv); + } else + { c = (char)('a' + lv - 10); + } v >>= 4; ret = c + ret; @@ -522,10 +822,14 @@ namespace Godot internal static bool HtmlIsValid(string color) { if (color.Length == 0) + { return false; + } if (color[0] == '#') + { color = color.Substring(1, color.Length - 1); + } bool alpha; @@ -544,83 +848,27 @@ namespace Godot if (alpha) { if (ParseCol8(color, 0) < 0) + { return false; + } } int from = alpha ? 2 : 0; if (ParseCol8(color, from + 0) < 0) - return false; - if (ParseCol8(color, from + 2) < 0) - return false; - if (ParseCol8(color, from + 4) < 0) - return false; - - return true; - } - - public static Color Color8(byte r8, byte g8, byte b8, byte a8 = 255) - { - return new Color(r8 / 255f, g8 / 255f, b8 / 255f, a8 / 255f); - } - - public Color(string rgba) - { - if (rgba.Length == 0) { - r = 0f; - g = 0f; - b = 0f; - a = 1.0f; - return; - } - - if (rgba[0] == '#') - rgba = rgba.Substring(1); - - bool alpha; - - if (rgba.Length == 8) - { - alpha = true; - } - else if (rgba.Length == 6) - { - alpha = false; - } - else - { - throw new ArgumentOutOfRangeException("Invalid color code. Length is " + rgba.Length + " but a length of 6 or 8 is expected: " + rgba); + return false; } - - if (alpha) + if (ParseCol8(color, from + 2) < 0) { - a = ParseCol8(rgba, 0) / 255f; - - if (a < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Alpha part is not valid hexadecimal: " + rgba); + return false; } - else + if (ParseCol8(color, from + 4) < 0) { - a = 1.0f; + return false; } - int from = alpha ? 2 : 0; - - r = ParseCol8(rgba, from + 0) / 255f; - - if (r < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Red part is not valid hexadecimal: " + rgba); - - g = ParseCol8(rgba, from + 2) / 255f; - - if (g < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Green part is not valid hexadecimal: " + rgba); - - b = ParseCol8(rgba, from + 4) / 255f; - - if (b < 0) - throw new ArgumentOutOfRangeException("Invalid color code. Blue part is not valid hexadecimal: " + rgba); + return true; } public static Color operator +(Color left, Color right) @@ -708,13 +956,13 @@ namespace Godot if (Mathf.IsEqualApprox(left.g, right.g)) { if (Mathf.IsEqualApprox(left.b, right.b)) + { return left.a < right.a; + } return left.b < right.b; } - return left.g < right.g; } - return left.r < right.r; } @@ -725,13 +973,13 @@ namespace Godot if (Mathf.IsEqualApprox(left.g, right.g)) { if (Mathf.IsEqualApprox(left.b, right.b)) + { return left.a > right.a; + } return left.b > right.b; } - return left.g > right.g; } - return left.r > right.r; } @@ -750,6 +998,12 @@ namespace Godot return r == other.r && g == other.g && b == other.b && a == other.a; } + /// <summary> + /// Returns true if this color and `other` are approximately equal, by running + /// <see cref="Godot.Mathf.IsEqualApprox(float, float)"/> on each component. + /// </summary> + /// <param name="other">The other color to compare.</param> + /// <returns>Whether or not the colors are approximately equal.</returns> public bool IsEqualApprox(Color other) { return Mathf.IsEqualApprox(r, other.r) && Mathf.IsEqualApprox(g, other.g) && Mathf.IsEqualApprox(b, other.b) && Mathf.IsEqualApprox(a, other.a); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs index f41f5e9fc8..d05a0414aa 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs @@ -3,6 +3,10 @@ using System.Collections.Generic; namespace Godot { + /// <summary> + /// This class contains color constants created from standardized color names. + /// The standardized color set is based on the X11 and .NET color names. + /// </summary> public static class Colors { // Color names and values are derived from core/color_names.inc diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs index 4f7aa99df8..6eecc262d6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs @@ -11,79 +11,185 @@ namespace Godot { // Define constants with Decimal precision and cast down to double or float. + /// <summary> + /// The circle constant, the circumference of the unit circle in radians. + /// </summary> public const real_t Tau = (real_t) 6.2831853071795864769252867666M; // 6.2831855f and 6.28318530717959 + + /// <summary> + /// Constant that represents how many times the diameter of a circle + /// fits around its perimeter. This is equivalent to `Mathf.Tau / 2`. + /// </summary> public const real_t Pi = (real_t) 3.1415926535897932384626433833M; // 3.1415927f and 3.14159265358979 + + /// <summary> + /// Positive infinity. For negative infinity, use `-Mathf.Inf`. + /// </summary> public const real_t Inf = real_t.PositiveInfinity; + + /// <summary> + /// "Not a Number", an invalid value. `NaN` has special properties, including + /// that it is not equal to itself. It is output by some invalid operations, + /// such as dividing zero by zero. + /// </summary> public const real_t NaN = real_t.NaN; private const real_t Deg2RadConst = (real_t) 0.0174532925199432957692369077M; // 0.0174532924f and 0.0174532925199433 private const real_t Rad2DegConst = (real_t) 57.295779513082320876798154814M; // 57.29578f and 57.2957795130823 + /// <summary> + /// Returns the absolute value of `s` (i.e. positive value). + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>The absolute value of `s`.</returns> public static int Abs(int s) { return Math.Abs(s); } + /// <summary> + /// Returns the absolute value of `s` (i.e. positive value). + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>The absolute value of `s`.</returns> public static real_t Abs(real_t s) { return Math.Abs(s); } + /// <summary> + /// Returns the arc cosine of `s` in radians. Use to get the angle of cosine s. + /// </summary> + /// <param name="s">The input cosine value. Must be on the range of -1.0 to 1.0.</param> + /// <returns>An angle that would result in the given cosine value. On the range `0` to `Tau/2`.</returns> public static real_t Acos(real_t s) { return (real_t)Math.Acos(s); } + /// <summary> + /// Returns the arc sine of `s` in radians. Use to get the angle of sine s. + /// </summary> + /// <param name="s">The input sine value. Must be on the range of -1.0 to 1.0.</param> + /// <returns>An angle that would result in the given sine value. On the range `-Tau/4` to `Tau/4`.</returns> public static real_t Asin(real_t s) { return (real_t)Math.Asin(s); } + /// <summary> + /// Returns the arc tangent of `s` in radians. Use to get the angle of tangent s. + /// + /// The method cannot know in which quadrant the angle should fall. + /// See <see cref="Atan2(real_t, real_t)"/> if you have both `y` and `x`. + /// </summary> + /// <param name="s">The input tangent value.</param> + /// <returns>An angle that would result in the given tangent value. On the range `-Tau/4` to `Tau/4`.</returns> public static real_t Atan(real_t s) { return (real_t)Math.Atan(s); } + /// <summary> + /// Returns the arc tangent of `y` and `x` in radians. Use to get the angle + /// of the tangent of `y/x`. To compute the value, the method takes into + /// account the sign of both arguments in order to determine the quadrant. + /// + /// Important note: The Y coordinate comes first, by convention. + /// </summary> + /// <param name="y">The Y coordinate of the point to find the angle to.</param> + /// <param name="x">The X coordinate of the point to find the angle to.</param> + /// <returns>An angle that would result in the given tangent value. On the range `-Tau/2` to `Tau/2`.</returns> public static real_t Atan2(real_t y, real_t x) { return (real_t)Math.Atan2(y, x); } + /// <summary> + /// Converts a 2D point expressed in the cartesian coordinate + /// system (X and Y axis) to the polar coordinate system + /// (a distance from the origin and an angle). + /// </summary> + /// <param name="x">The input X coordinate.</param> + /// <param name="y">The input Y coordinate.</param> + /// <returns>A <see cref="Vector2"/> with X representing the distance and Y representing the angle.</returns> public static Vector2 Cartesian2Polar(real_t x, real_t y) { return new Vector2(Sqrt(x * x + y * y), Atan2(y, x)); } + /// <summary> + /// Rounds `s` upward (towards positive infinity). + /// </summary> + /// <param name="s">The number to ceil.</param> + /// <returns>The smallest whole number that is not less than `s`.</returns> public static real_t Ceil(real_t s) { return (real_t)Math.Ceiling(s); } + /// <summary> + /// Clamps a `value` so that it is not less than `min` and not more than `max`. + /// </summary> + /// <param name="value">The value to clamp.</param> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The clamped value.</returns> public static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } + /// <summary> + /// Clamps a `value` so that it is not less than `min` and not more than `max`. + /// </summary> + /// <param name="value">The value to clamp.</param> + /// <param name="min">The minimum allowed value.</param> + /// <param name="max">The maximum allowed value.</param> + /// <returns>The clamped value.</returns> public static real_t Clamp(real_t value, real_t min, real_t max) { return value < min ? min : value > max ? max : value; } + /// <summary> + /// Returns the cosine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The cosine of that angle.</returns> public static real_t Cos(real_t s) { return (real_t)Math.Cos(s); } + /// <summary> + /// Returns the hyperbolic cosine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic cosine of that angle.</returns> public static real_t Cosh(real_t s) { return (real_t)Math.Cosh(s); } + /// <summary> + /// Converts an angle expressed in degrees to radians. + /// </summary> + /// <param name="deg">An angle expressed in degrees.</param> + /// <returns>The same angle expressed in radians.</returns> public static real_t Deg2Rad(real_t deg) { return deg * Deg2RadConst; } + /// <summary> + /// Easing function, based on exponent. The curve values are: + /// `0` is constant, `1` is linear, `0` to `1` is ease-in, `1` or more is ease-out. + /// Negative values are in-out/out-in. + /// </summary> + /// <param name="s">The value to ease.</param> + /// <param name="curve">`0` is constant, `1` is linear, `0` to `1` is ease-in, `1` or more is ease-out.</param> + /// <returns>The eased value.</returns> public static real_t Ease(real_t s, real_t curve) { if (s < 0f) @@ -118,21 +224,47 @@ namespace Godot return 0f; } + /// <summary> + /// The natural exponential function. It raises the mathematical + /// constant `e` to the power of `s` and returns it. + /// </summary> + /// <param name="s">The exponent to raise `e` to.</param> + /// <returns>`e` raised to the power of `s`.</returns> public static real_t Exp(real_t s) { return (real_t)Math.Exp(s); } + /// <summary> + /// Rounds `s` downward (towards negative infinity). + /// </summary> + /// <param name="s">The number to floor.</param> + /// <returns>The largest whole number that is not more than `s`.</returns> public static real_t Floor(real_t s) { return (real_t)Math.Floor(s); } + /// <summary> + /// Returns a normalized value considering the given range. + /// This is the opposite of <see cref="Lerp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="from">The interpolated value.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting value of the inverse interpolation.</returns> public static real_t InverseLerp(real_t from, real_t to, real_t weight) { return (weight - from) / (to - from); } + /// <summary> + /// Returns true if `a` and `b` are approximately equal to each other. + /// The comparison is done using a tolerance calculation with <see cref="Epsilon"/>. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>A bool for whether or not the two values are approximately equal.</returns> public static bool IsEqualApprox(real_t a, real_t b) { // Check for exact equality first, required to handle "infinity" values. @@ -149,26 +281,62 @@ namespace Godot return Abs(a - b) < tolerance; } + /// <summary> + /// Returns whether `s` is an infinity value (either positive infinity or negative infinity). + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is an infinity value.</returns> public static bool IsInf(real_t s) { return real_t.IsInfinity(s); } + /// <summary> + /// Returns whether `s` is a `NaN` ("Not a Number" or invalid) value. + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is a `NaN` value.</returns> public static bool IsNaN(real_t s) { return real_t.IsNaN(s); } + /// <summary> + /// Returns true if `s` is approximately zero. + /// The comparison is done using a tolerance calculation with <see cref="Epsilon"/>. + /// + /// This method is faster than using <see cref="IsEqualApprox(real_t, real_t)"/> with one value as zero. + /// </summary> + /// <param name="s">The value to check.</param> + /// <returns>A bool for whether or not the value is nearly zero.</returns> public static bool IsZeroApprox(real_t s) { return Abs(s) < Epsilon; } + /// <summary> + /// Linearly interpolates between two values by a normalized value. + /// This is the opposite <see cref="InverseLerp(real_t, real_t, real_t)"/>. + /// </summary> + /// <param name="from">The start value for interpolation.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting value of the interpolation.</returns> public static real_t Lerp(real_t from, real_t to, real_t weight) { return from + (to - from) * weight; } + /// <summary> + /// Linearly interpolates between two angles (in radians) by a normalized value. + /// + /// Similar to <see cref="Lerp(real_t, real_t, real_t)"/>, + /// but interpolates correctly when the angles wrap around <see cref="Tau"/>. + /// </summary> + /// <param name="from">The start angle for interpolation.</param> + /// <param name="to">The destination angle for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting angle of the interpolation.</returns> public static real_t LerpAngle(real_t from, real_t to, real_t weight) { real_t difference = (to - from) % Mathf.Tau; @@ -176,36 +344,81 @@ namespace Godot return from + distance * weight; } + /// <summary> + /// Natural logarithm. The amount of time needed to reach a certain level of continuous growth. + /// + /// Note: This is not the same as the "log" function on most calculators, which uses a base 10 logarithm. + /// </summary> + /// <param name="s">The input value.</param> + /// <returns>The natural log of `s`.</returns> public static real_t Log(real_t s) { return (real_t)Math.Log(s); } + /// <summary> + /// Returns the maximum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is higher.</returns> public static int Max(int a, int b) { return a > b ? a : b; } + /// <summary> + /// Returns the maximum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is higher.</returns> public static real_t Max(real_t a, real_t b) { return a > b ? a : b; } + /// <summary> + /// Returns the minimum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is lower.</returns> public static int Min(int a, int b) { return a < b ? a : b; } + /// <summary> + /// Returns the minimum of two values. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <returns>Whichever of the two values is lower.</returns> public static real_t Min(real_t a, real_t b) { return a < b ? a : b; } + /// <summary> + /// Moves `from` toward `to` by the `delta` value. + /// + /// Use a negative delta value to move away. + /// </summary> + /// <param name="from">The start value.</param> + /// <param name="to">The value to move towards.</param> + /// <param name="delta">The amount to move by.</param> + /// <returns>The value after moving.</returns> public static real_t MoveToward(real_t from, real_t to, real_t delta) { return Abs(to - from) <= delta ? to : from + Sign(to - from) * delta; } + /// <summary> + /// Returns the nearest larger power of 2 for the integer `value`. + /// </summary> + /// <param name="value">The input value.</param> + /// <returns>The nearest larger power of 2.</returns> public static int NearestPo2(int value) { value--; @@ -218,14 +431,25 @@ namespace Godot return value; } + /// <summary> + /// Converts a 2D point expressed in the polar coordinate + /// system (a distance from the origin `r` and an angle `th`) + /// to the cartesian coordinate system (X and Y axis). + /// </summary> + /// <param name="r">The distance from the origin.</param> + /// <param name="th">The angle of the point.</param> + /// <returns>A <see cref="Vector2"/> representing the cartesian coordinate.</returns> public static Vector2 Polar2Cartesian(real_t r, real_t th) { return new Vector2(r * Cos(th), r * Sin(th)); } /// <summary> - /// Performs a canonical Modulus operation, where the output is on the range [0, b). + /// Performs a canonical Modulus operation, where the output is on the range `[0, b)`. /// </summary> + /// <param name="a">The dividend, the primary input.</param> + /// <param name="b">The divisor. The output is on the range `[0, b)`.</param> + /// <returns>The resulting output.</returns> public static int PosMod(int a, int b) { int c = a % b; @@ -237,8 +461,11 @@ namespace Godot } /// <summary> - /// Performs a canonical Modulus operation, where the output is on the range [0, b). + /// Performs a canonical Modulus operation, where the output is on the range `[0, b)`. /// </summary> + /// <param name="a">The dividend, the primary input.</param> + /// <param name="b">The divisor. The output is on the range `[0, b)`.</param> + /// <returns>The resulting output.</returns> public static real_t PosMod(real_t a, real_t b) { real_t c = a % b; @@ -249,43 +476,89 @@ namespace Godot return c; } + /// <summary> + /// Returns the result of `x` raised to the power of `y`. + /// </summary> + /// <param name="x">The base.</param> + /// <param name="y">The exponent.</param> + /// <returns>`x` raised to the power of `y`.</returns> public static real_t Pow(real_t x, real_t y) { return (real_t)Math.Pow(x, y); } + /// <summary> + /// Converts an angle expressed in radians to degrees. + /// </summary> + /// <param name="rad">An angle expressed in radians.</param> + /// <returns>The same angle expressed in degrees.</returns> public static real_t Rad2Deg(real_t rad) { return rad * Rad2DegConst; } + /// <summary> + /// Rounds `s` to the nearest whole number, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <param name="s">The number to round.</param> + /// <returns>The rounded number.</returns> public static real_t Round(real_t s) { return (real_t)Math.Round(s); } + /// <summary> + /// Returns the sign of `s`: `-1` or `1`. Returns `0` if `s` is `0`. + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>One of three possible values: `1`, `-1`, or `0`.</returns> public static int Sign(int s) { if (s == 0) return 0; return s < 0 ? -1 : 1; } + /// <summary> + /// Returns the sign of `s`: `-1` or `1`. Returns `0` if `s` is `0`. + /// </summary> + /// <param name="s">The input number.</param> + /// <returns>One of three possible values: `1`, `-1`, or `0`.</returns> public static int Sign(real_t s) { if (s == 0) return 0; return s < 0 ? -1 : 1; } + /// <summary> + /// Returns the sine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The sine of that angle.</returns> public static real_t Sin(real_t s) { return (real_t)Math.Sin(s); } + /// <summary> + /// Returns the hyperbolic sine of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic sine of that angle.</returns> public static real_t Sinh(real_t s) { return (real_t)Math.Sinh(s); } + /// <summary> + /// Returns a number smoothly interpolated between `from` and `to`, + /// based on the `weight`. Similar to <see cref="Lerp(real_t, real_t, real_t)"/>, + /// but interpolates faster at the beginning and slower at the end. + /// </summary> + /// <param name="from">The start value for interpolation.</param> + /// <param name="to">The destination value for interpolation.</param> + /// <param name="weight">A value representing the amount of interpolation.</param> + /// <returns>The resulting value of the interpolation.</returns> public static real_t SmoothStep(real_t from, real_t to, real_t weight) { if (IsEqualApprox(from, to)) @@ -296,11 +569,25 @@ namespace Godot return x * x * (3 - 2 * x); } + /// <summary> + /// Returns the square root of `s`, where `s` is a non-negative number. + /// + /// If you need negative inputs, use `System.Numerics.Complex`. + /// </summary> + /// <param name="s">The input number. Must not be negative.</param> + /// <returns>The square root of `s`.</returns> public static real_t Sqrt(real_t s) { return (real_t)Math.Sqrt(s); } + /// <summary> + /// Returns the position of the first non-zero digit, after the + /// decimal point. Note that the maximum return value is 10, + /// which is a design decision in the implementation. + /// </summary> + /// <param name="step">The input value.</param> + /// <returns>The position of the first non-zero digit.</returns> public static int StepDecimals(real_t step) { double[] sd = new double[] { @@ -326,32 +613,68 @@ namespace Godot return 0; } + /// <summary> + /// Snaps float value `s` to a given `step`. + /// This can also be used to round a floating point + /// number to an arbitrary number of decimals. + /// </summary> + /// <param name="s">The value to stepify.</param> + /// <param name="step">The step size to snap to.</param> + /// <returns></returns> public static real_t Stepify(real_t s, real_t step) { if (step != 0f) { - s = Floor(s / step + 0.5f) * step; + return Floor(s / step + 0.5f) * step; } return s; } + /// <summary> + /// Returns the tangent of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The tangent of that angle.</returns> public static real_t Tan(real_t s) { return (real_t)Math.Tan(s); } + /// <summary> + /// Returns the hyperbolic tangent of angle `s` in radians. + /// </summary> + /// <param name="s">The angle in radians.</param> + /// <returns>The hyperbolic tangent of that angle.</returns> public static real_t Tanh(real_t s) { return (real_t)Math.Tanh(s); } + /// <summary> + /// Wraps `value` between `min` and `max`. Usable for creating loop-alike + /// behavior or infinite surfaces. If `min` is `0`, this is equivalent + /// to <see cref="PosMod(int, int)"/>, so prefer using that instead. + /// </summary> + /// <param name="value">The value to wrap.</param> + /// <param name="min">The minimum allowed value and lower bound of the range.</param> + /// <param name="max">The maximum allowed value and upper bound of the range.</param> + /// <returns>The wrapped value.</returns> public static int Wrap(int value, int min, int max) { int range = max - min; return range == 0 ? min : min + ((value - min) % range + range) % range; } + /// <summary> + /// Wraps `value` between `min` and `max`. Usable for creating loop-alike + /// behavior or infinite surfaces. If `min` is `0`, this is equivalent + /// to <see cref="PosMod(real_t, real_t)"/>, so prefer using that instead. + /// </summary> + /// <param name="value">The value to wrap.</param> + /// <param name="min">The minimum allowed value and lower bound of the range.</param> + /// <param name="max">The maximum allowed value and upper bound of the range.</param> + /// <returns>The wrapped value.</returns> public static real_t Wrap(real_t value, real_t min, real_t max) { real_t range = max - min; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs index 1b7fd4906f..c2f4701b5f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs @@ -12,40 +12,89 @@ namespace Godot { // Define constants with Decimal precision and cast down to double or float. + /// <summary> + /// The natural number `e`. + /// </summary> public const real_t E = (real_t) 2.7182818284590452353602874714M; // 2.7182817f and 2.718281828459045 + + /// <summary> + /// The square root of 2. + /// </summary> public const real_t Sqrt2 = (real_t) 1.4142135623730950488016887242M; // 1.4142136f and 1.414213562373095 + /// <summary> + /// A very small number used for float comparison with error tolerance. + /// 1e-06 with single-precision floats, but 1e-14 if `REAL_T_IS_DOUBLE`. + /// </summary> #if REAL_T_IS_DOUBLE public const real_t Epsilon = 1e-14; // Epsilon size should depend on the precision used. #else public const real_t Epsilon = 1e-06f; #endif + /// <summary> + /// Returns the amount of digits after the decimal place. + /// </summary> + /// <param name="s">The input value.</param> + /// <returns>The amount of digits.</returns> public static int DecimalCount(real_t s) { return DecimalCount((decimal)s); } + /// <summary> + /// Returns the amount of digits after the decimal place. + /// </summary> + /// <param name="s">The input <see cref="System.Decimal"/> value.</param> + /// <returns>The amount of digits.</returns> public static int DecimalCount(decimal s) { return BitConverter.GetBytes(decimal.GetBits(s)[3])[2]; } + /// <summary> + /// Rounds `s` upward (towards positive infinity). + /// + /// This is the same as <see cref="Ceil(real_t)"/>, but returns an `int`. + /// </summary> + /// <param name="s">The number to ceil.</param> + /// <returns>The smallest whole number that is not less than `s`.</returns> public static int CeilToInt(real_t s) { return (int)Math.Ceiling(s); } + /// <summary> + /// Rounds `s` downward (towards negative infinity). + /// + /// This is the same as <see cref="Floor(real_t)"/>, but returns an `int`. + /// </summary> + /// <param name="s">The number to floor.</param> + /// <returns>The largest whole number that is not more than `s`.</returns> public static int FloorToInt(real_t s) { return (int)Math.Floor(s); } + /// <summary> + /// + /// </summary> + /// <param name="s"></param> + /// <returns></returns> public static int RoundToInt(real_t s) { return (int)Math.Round(s); } + /// <summary> + /// Returns true if `a` and `b` are approximately equal to each other. + /// The comparison is done using the provided tolerance value. + /// If you want the tolerance to be calculated for you, use <see cref="IsEqualApprox(real_t, real_t)"/>. + /// </summary> + /// <param name="a">One of the values.</param> + /// <param name="b">The other value.</param> + /// <param name="tolerance">The pre-calculated tolerance value.</param> + /// <returns>A bool for whether or not the two values are equal.</returns> public static bool IsEqualApprox(real_t a, real_t b, real_t tolerance) { // Check for exact equality first, required to handle "infinity" values. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs index 885845e3a4..2f8b5f297c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs @@ -8,18 +8,33 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// Plane represents a normalized plane equation. + /// "Over" or "Above" the plane is considered the side of + /// the plane towards where the normal is pointing. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Plane : IEquatable<Plane> { private Vector3 _normal; + /// <summary> + /// The normal of the plane, which must be normalized. + /// In the scalar equation of the plane `ax + by + cz = d`, this is + /// the vector `(a, b, c)`, where `d` is the <see cref="D"/> property. + /// </summary> + /// <value>Equivalent to `x`, `y`, and `z`.</value> public Vector3 Normal { get { return _normal; } set { _normal = value; } } + /// <summary> + /// The X component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s X value.</value> public real_t x { get @@ -32,6 +47,10 @@ namespace Godot } } + /// <summary> + /// The Y component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s Y value.</value> public real_t y { get @@ -44,6 +63,10 @@ namespace Godot } } + /// <summary> + /// The Z component of the plane's normal vector. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/>'s Z value.</value> public real_t z { get @@ -56,38 +79,71 @@ namespace Godot } } + /// <summary> + /// The distance from the origin to the plane (in the direction of + /// <see cref="Normal"/>). This value is typically non-negative. + /// In the scalar equation of the plane `ax + by + cz = d`, + /// this is `d`, while the `(a, b, c)` coordinates are represented + /// by the <see cref="Normal"/> property. + /// </summary> + /// <value>The plane's distance from the origin.</value> public real_t D { get; set; } + /// <summary> + /// The center of the plane, the point where the normal line intersects the plane. + /// </summary> + /// <value>Equivalent to <see cref="Normal"/> multiplied by `D`.</value> public Vector3 Center { get { return _normal * D; } + set + { + _normal = value.Normalized(); + D = value.Length(); + } } + /// <summary> + /// Returns the shortest distance from this plane to the position `point`. + /// </summary> + /// <param name="point">The position to use for the calculation.</param> + /// <returns>The shortest distance.</returns> public real_t DistanceTo(Vector3 point) { return _normal.Dot(point) - D; } - public Vector3 GetAnyPoint() - { - return _normal * D; - } - + /// <summary> + /// Returns true if point is inside the plane. + /// Comparison uses a custom minimum epsilon threshold. + /// </summary> + /// <param name="point">The point to check.</param> + /// <param name="epsilon">The tolerance threshold.</param> + /// <returns>A bool for whether or not the plane has the point.</returns> public bool HasPoint(Vector3 point, real_t epsilon = Mathf.Epsilon) { real_t dist = _normal.Dot(point) - D; return Mathf.Abs(dist) <= epsilon; } + /// <summary> + /// Returns the intersection point of the three planes: `b`, `c`, + /// and this plane. If no intersection is found, `null` is returned. + /// </summary> + /// <param name="b">One of the three planes to use in the calculation.</param> + /// <param name="c">One of the three planes to use in the calculation.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? Intersect3(Plane b, Plane c) { real_t denom = _normal.Cross(b._normal).Dot(c._normal); if (Mathf.IsZeroApprox(denom)) + { return null; + } Vector3 result = b._normal.Cross(c._normal) * D + c._normal.Cross(_normal) * b.D + @@ -96,54 +152,94 @@ namespace Godot return result / denom; } + /// <summary> + /// Returns the intersection point of a ray consisting of the + /// position `from` and the direction normal `dir` with this plane. + /// If no intersection is found, `null` is returned. + /// </summary> + /// <param name="from">The start of the ray.</param> + /// <param name="dir">The direction of the ray, normalized.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? IntersectRay(Vector3 from, Vector3 dir) { real_t den = _normal.Dot(dir); if (Mathf.IsZeroApprox(den)) + { return null; + } real_t dist = (_normal.Dot(from) - D) / den; // This is a ray, before the emitting pos (from) does not exist if (dist > Mathf.Epsilon) + { return null; + } return from + dir * -dist; } + /// <summary> + /// Returns the intersection point of a line segment from + /// position `begin` to position `end` with this plane. + /// If no intersection is found, `null` is returned. + /// </summary> + /// <param name="begin">The start of the line segment.</param> + /// <param name="end">The end of the line segment.</param> + /// <returns>The intersection, or `null` if none is found.</returns> public Vector3? IntersectSegment(Vector3 begin, Vector3 end) { Vector3 segment = begin - end; real_t den = _normal.Dot(segment); if (Mathf.IsZeroApprox(den)) + { return null; + } real_t dist = (_normal.Dot(begin) - D) / den; // Only allow dist to be in the range of 0 to 1, with tolerance. if (dist < -Mathf.Epsilon || dist > 1.0f + Mathf.Epsilon) + { return null; + } return begin + segment * -dist; } + /// <summary> + /// Returns true if `point` is located above the plane. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the point is above the plane.</returns> public bool IsPointOver(Vector3 point) { return _normal.Dot(point) > D; } + /// <summary> + /// Returns the plane scaled to unit length. + /// </summary> + /// <returns>A normalized version of the plane.</returns> public Plane Normalized() { real_t len = _normal.Length(); if (len == 0) + { return new Plane(0, 0, 0, 0); + } return new Plane(_normal / len, D / len); } + /// <summary> + /// Returns the orthogonal projection of `point` into the plane. + /// </summary> + /// <param name="point">The point to project.</param> + /// <returns>The projected point.</returns> public Vector3 Project(Vector3 point) { return point - _normal * DistanceTo(point); @@ -154,22 +250,56 @@ namespace Godot private static readonly Plane _planeXZ = new Plane(0, 1, 0, 0); private static readonly Plane _planeXY = new Plane(0, 0, 1, 0); + /// <summary> + /// A plane that extends in the Y and Z axes (normal vector points +X). + /// </summary> + /// <value>Equivalent to `new Plane(1, 0, 0, 0)`.</value> public static Plane PlaneYZ { get { return _planeYZ; } } + + /// <summary> + /// A plane that extends in the X and Z axes (normal vector points +Y). + /// </summary> + /// <value>Equivalent to `new Plane(0, 1, 0, 0)`.</value> public static Plane PlaneXZ { get { return _planeXZ; } } + + /// <summary> + /// A plane that extends in the X and Y axes (normal vector points +Z). + /// </summary> + /// <value>Equivalent to `new Plane(0, 0, 1, 0)`.</value> public static Plane PlaneXY { get { return _planeXY; } } - // Constructors + /// <summary> + /// Constructs a plane from four values. `a`, `b` and `c` become the + /// components of the resulting plane's <see cref="Normal"/> vector. + /// `d` becomes the plane's distance from the origin. + /// </summary> + /// <param name="a">The X component of the plane's normal vector.</param> + /// <param name="b">The Y component of the plane's normal vector.</param> + /// <param name="c">The Z component of the plane's normal vector.</param> + /// <param name="d">The plane's distance from the origin. This value is typically non-negative.</param> public Plane(real_t a, real_t b, real_t c, real_t d) { _normal = new Vector3(a, b, c); this.D = d; } + + /// <summary> + /// Constructs a plane from a normal vector and the plane's distance to the origin. + /// </summary> + /// <param name="normal">The normal of the plane, must be normalized.</param> + /// <param name="d">The plane's distance from the origin. This value is typically non-negative.</param> public Plane(Vector3 normal, real_t d) { this._normal = normal; this.D = d; } + /// <summary> + /// Constructs a plane from the three points, given in clockwise order. + /// </summary> + /// <param name="v1">The first point.</param> + /// <param name="v2">The second point.</param> + /// <param name="v3">The third point.</param> public Plane(Vector3 v1, Vector3 v2, Vector3 v3) { _normal = (v1 - v3).Cross(v1 - v2); @@ -207,6 +337,12 @@ namespace Godot return _normal == other._normal && D == other.D; } + /// <summary> + /// Returns true if this plane and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other plane to compare.</param> + /// <returns>Whether or not the planes are approximately equal.</returns> public bool IsEqualApprox(Plane other) { return _normal.IsEqualApprox(other._normal) && Mathf.IsEqualApprox(D, other.D); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs index bbc617ea6e..b33490f9cb 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quat.cs @@ -8,15 +8,51 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// A unit quaternion used for representing 3D rotations. + /// Quaternions need to be normalized to be used for rotation. + /// + /// It is similar to Basis, which implements matrix representation of + /// rotations, and can be parametrized using both an axis-angle pair + /// or Euler angles. Basis stores rotation, scale, and shearing, + /// while Quat only stores rotation. + /// + /// Due to its compactness and the way it is stored in memory, certain + /// operations (obtaining axis-angle and performing SLERP, in particular) + /// are more efficient and robust against floating-point errors. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Quat : IEquatable<Quat> { + /// <summary> + /// X component of the quaternion (imaginary `i` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t x; + + /// <summary> + /// Y component of the quaternion (imaginary `j` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t y; + + /// <summary> + /// Z component of the quaternion (imaginary `k` axis part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t z; + + /// <summary> + /// W component of the quaternion (real part). + /// Quaternion components should usually not be manipulated directly. + /// </summary> public real_t w; + /// <summary> + /// Access quaternion components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`, `[3]` is equivalent to `.w`.</value> public real_t this[int index] { get @@ -57,16 +93,35 @@ namespace Godot } } + /// <summary> + /// Returns the length (magnitude) of the quaternion. + /// </summary> + /// <value>Equivalent to `Mathf.Sqrt(LengthSquared)`.</value> public real_t Length { get { return Mathf.Sqrt(LengthSquared); } } + /// <summary> + /// Returns the squared length (squared magnitude) of the quaternion. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare quaternions or need the squared length for some formula. + /// </summary> + /// <value>Equivalent to `Dot(this)`.</value> public real_t LengthSquared { get { return Dot(this); } } + /// <summary> + /// Performs a cubic spherical interpolation between quaternions `preA`, + /// this vector, `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination quaternion.</param> + /// <param name="preA">A quaternion before this quaternion.</param> + /// <param name="postB">A quaternion after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated quaternion.</returns> public Quat CubicSlerp(Quat b, Quat preA, Quat postB, real_t t) { real_t t2 = (1.0f - t) * t * 2f; @@ -75,85 +130,131 @@ namespace Godot return sp.Slerpni(sq, t2); } + /// <summary> + /// Returns the dot product of two quaternions. + /// </summary> + /// <param name="b">The other quaternion.</param> + /// <returns>The dot product.</returns> public real_t Dot(Quat b) { return x * b.x + y * b.y + z * b.z + w * b.w; } + /// <summary> + /// Returns Euler angles (in the YXZ convention: when decomposing, + /// first Z, then X, and Y last) corresponding to the rotation + /// represented by the unit quaternion. Returned vector contains + /// the rotation angles in the format (X angle, Y angle, Z angle). + /// </summary> + /// <returns>The Euler angle representation of this quaternion.</returns> public Vector3 GetEuler() { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif var basis = new Basis(this); return basis.GetEuler(); } + /// <summary> + /// Returns the inverse of the quaternion. + /// </summary> + /// <returns>The inverse quaternion.</returns> public Quat Inverse() { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif return new Quat(-x, -y, -z, w); } + /// <summary> + /// Returns whether the quaternion is normalized or not. + /// </summary> + /// <returns>A bool for whether the quaternion is normalized or not.</returns> + public bool IsNormalized() + { + return Mathf.Abs(LengthSquared - 1) <= Mathf.Epsilon; + } + + /// <summary> + /// Returns a copy of the quaternion, normalized to unit length. + /// </summary> + /// <returns>The normalized quaternion.</returns> public Quat Normalized() { return this / Length; } - public Quat Slerp(Quat b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this quaternion and `to` by amount `weight`. + /// + /// Note: Both quaternions must be normalized. + /// </summary> + /// <param name="to">The destination quaternion for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting quaternion of the interpolation.</returns> + public Quat Slerp(Quat to, real_t weight) { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); - if (!b.IsNormalized()) - throw new ArgumentException("Argument is not normalized", nameof(b)); + } + if (!to.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(to)); + } #endif - // Calculate cosine - real_t cosom = x * b.x + y * b.y + z * b.z + w * b.w; + // Calculate cosine. + real_t cosom = x * to.x + y * to.y + z * to.z + w * to.w; var to1 = new Quat(); - // Adjust signs if necessary + // Adjust signs if necessary. if (cosom < 0.0) { cosom = -cosom; - to1.x = -b.x; - to1.y = -b.y; - to1.z = -b.z; - to1.w = -b.w; + to1.x = -to.x; + to1.y = -to.y; + to1.z = -to.z; + to1.w = -to.w; } else { - to1.x = b.x; - to1.y = b.y; - to1.z = b.z; - to1.w = b.w; + to1.x = to.x; + to1.y = to.y; + to1.z = to.z; + to1.w = to.w; } real_t sinom, scale0, scale1; - // Calculate coefficients + // Calculate coefficients. if (1.0 - cosom > Mathf.Epsilon) { - // Standard case (Slerp) + // Standard case (Slerp). real_t omega = Mathf.Acos(cosom); sinom = Mathf.Sin(omega); - scale0 = Mathf.Sin((1.0f - t) * omega) / sinom; - scale1 = Mathf.Sin(t * omega) / sinom; + scale0 = Mathf.Sin((1.0f - weight) * omega) / sinom; + scale1 = Mathf.Sin(weight * omega) / sinom; } else { - // Quaternions are very close so we can do a linear interpolation - scale0 = 1.0f - t; - scale1 = t; + // Quaternions are very close so we can do a linear interpolation. + scale0 = 1.0f - weight; + scale1 = weight; } - // Calculate final values + // Calculate final values. return new Quat ( scale0 * x + scale1 * to1.x, @@ -163,9 +264,17 @@ namespace Godot ); } - public Quat Slerpni(Quat b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this quaternion and `to` by amount `weight`, but without + /// checking if the rotation path is not bigger than 90 degrees. + /// </summary> + /// <param name="to">The destination quaternion for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting quaternion of the interpolation.</returns> + public Quat Slerpni(Quat to, real_t weight) { - real_t dot = Dot(b); + real_t dot = Dot(to); if (Mathf.Abs(dot) > 0.9999f) { @@ -174,33 +283,54 @@ namespace Godot real_t theta = Mathf.Acos(dot); real_t sinT = 1.0f / Mathf.Sin(theta); - real_t newFactor = Mathf.Sin(t * theta) * sinT; - real_t invFactor = Mathf.Sin((1.0f - t) * theta) * sinT; + real_t newFactor = Mathf.Sin(weight * theta) * sinT; + real_t invFactor = Mathf.Sin((1.0f - weight) * theta) * sinT; return new Quat ( - invFactor * x + newFactor * b.x, - invFactor * y + newFactor * b.y, - invFactor * z + newFactor * b.z, - invFactor * w + newFactor * b.w + invFactor * x + newFactor * to.x, + invFactor * y + newFactor * to.y, + invFactor * z + newFactor * to.z, + invFactor * w + newFactor * to.w ); } + /// <summary> + /// Returns a vector transformed (multiplied) by this quaternion. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { #if DEBUG if (!IsNormalized()) + { throw new InvalidOperationException("Quat is not normalized"); + } #endif var u = new Vector3(x, y, z); Vector3 uv = u.Cross(v); return v + ((uv * w) + u.Cross(uv)) * 2; } - // Static Readonly Properties - public static Quat Identity { get; } = new Quat(0f, 0f, 0f, 1f); - - // Constructors + // Constants + private static readonly Quat _identity = new Quat(0, 0, 0, 1); + + /// <summary> + /// The identity quaternion, representing no rotation. + /// Equivalent to an identity <see cref="Basis"/> matrix. If a vector is transformed by + /// an identity quaternion, it will not change. + /// </summary> + /// <value>Equivalent to `new Quat(0, 0, 0, 1)`.</value> + public static Quat Identity { get { return _identity; } } + + /// <summary> + /// Constructs a quaternion defined by the given values. + /// </summary> + /// <param name="x">X component of the quaternion (imaginary `i` axis part).</param> + /// <param name="y">Y component of the quaternion (imaginary `j` axis part).</param> + /// <param name="z">Z component of the quaternion (imaginary `k` axis part).</param> + /// <param name="w">W component of the quaternion (real part).</param> public Quat(real_t x, real_t y, real_t z, real_t w) { this.x = x; @@ -209,21 +339,31 @@ namespace Godot this.w = w; } - public bool IsNormalized() - { - return Mathf.Abs(LengthSquared - 1) <= Mathf.Epsilon; - } - + /// <summary> + /// Constructs a quaternion from the given quaternion. + /// </summary> + /// <param name="q">The existing quaternion.</param> public Quat(Quat q) { this = q; } + /// <summary> + /// Constructs a quaternion from the given <see cref="Basis"/>. + /// </summary> + /// <param name="basis">The basis to construct from.</param> public Quat(Basis basis) { this = basis.Quat(); } + /// <summary> + /// Constructs a quaternion that will perform a rotation specified by + /// Euler angles (in the YXZ convention: when decomposing, + /// first Z, then X, and Y last), + /// given in the vector format as (X angle, Y angle, Z angle). + /// </summary> + /// <param name="eulerYXZ"></param> public Quat(Vector3 eulerYXZ) { real_t half_a1 = eulerYXZ.y * 0.5f; @@ -247,11 +387,19 @@ namespace Godot w = sin_a1 * sin_a2 * sin_a3 + cos_a1 * cos_a2 * cos_a3; } + /// <summary> + /// Constructs a quaternion that will rotate around the given axis + /// by the specified angle. The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="angle">The angle to rotate, in radians.</param> public Quat(Vector3 axis, real_t angle) { #if DEBUG if (!axis.IsNormalized()) + { throw new ArgumentException("Argument is not normalized", nameof(axis)); + } #endif real_t d = axis.Length(); @@ -364,6 +512,12 @@ namespace Godot return x == other.x && y == other.y && z == other.z && w == other.w; } + /// <summary> + /// Returns true if this quaternion and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other quaternion to compare.</param> + /// <returns>Whether or not the quaternions are approximately equal.</returns> public bool IsEqualApprox(Quat other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y) && Mathf.IsEqualApprox(z, other.z) && Mathf.IsEqualApprox(w, other.w); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index 1098ffe4e5..f7703c77cc 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -8,6 +8,10 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 2D axis-aligned bounding box. Rect2 consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Rect2 : IEquatable<Rect2> @@ -15,29 +19,52 @@ namespace Godot private Vector2 _position; private Vector2 _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2 Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2 Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector2 End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// The area of this rect. + /// </summary> + /// <value>Equivalent to <see cref="GetArea()"/>.</value> public real_t Area { get { return GetArea(); } } + /// <summary> + /// Returns a Rect2 with equivalent position and size, modified so that + /// the top-left corner is the origin and width and height are positive. + /// </summary> + /// <returns>The modified rect.</returns> public Rect2 Abs() { Vector2 end = End; @@ -45,12 +72,19 @@ namespace Godot return new Rect2(topLeft, _size.Abs()); } + /// <summary> + /// Returns the intersection of this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The clipped rect.</returns> public Rect2 Clip(Rect2 b) { var newRect = b; if (!Intersects(newRect)) + { return new Rect2(); + } newRect._position.x = Mathf.Max(b._position.x, _position.x); newRect._position.y = Mathf.Max(b._position.y, _position.y); @@ -64,6 +98,11 @@ namespace Godot return newRect; } + /// <summary> + /// Returns true if this Rect2 completely encloses another one. + /// </summary> + /// <param name="b">The other rect that may be enclosed.</param> + /// <returns>A bool for whether or not this rect encloses `b`.</returns> public bool Encloses(Rect2 b) { return b._position.x >= _position.x && b._position.y >= _position.y && @@ -71,6 +110,11 @@ namespace Godot b._position.y + b._size.y < _position.y + _size.y; } + /// <summary> + /// Returns this Rect2 expanded to include a given point. + /// </summary> + /// <param name="to">The point to include.</param> + /// <returns>The expanded rect.</returns> public Rect2 Expand(Vector2 to) { var expanded = this; @@ -79,14 +123,22 @@ namespace Godot Vector2 end = expanded._position + expanded._size; if (to.x < begin.x) + { begin.x = to.x; + } if (to.y < begin.y) + { begin.y = to.y; + } if (to.x > end.x) + { end.x = to.x; + } if (to.y > end.y) + { end.y = to.y; + } expanded._position = begin; expanded._size = end - begin; @@ -94,11 +146,20 @@ namespace Godot return expanded; } + /// <summary> + /// Returns the area of the Rect2. + /// </summary> + /// <returns>The area.</returns> public real_t GetArea() { return _size.x * _size.y; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2 Grow(real_t by) { var g = this; @@ -111,6 +172,14 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards each direction individually. + /// </summary> + /// <param name="left">The amount to grow by on the left.</param> + /// <param name="top">The amount to grow by on the top.</param> + /// <param name="right">The amount to grow by on the right.</param> + /// <param name="bottom">The amount to grow by on the bottom.</param> + /// <returns>The grown rect.</returns> public Rect2 GrowIndividual(real_t left, real_t top, real_t right, real_t bottom) { var g = this; @@ -123,6 +192,12 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2 grown a given amount of units towards the <see cref="Margin"/> direction. + /// </summary> + /// <param name="margin">The direction to grow in.</param> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2 GrowMargin(Margin margin, real_t by) { var g = this; @@ -135,11 +210,20 @@ namespace Godot return g; } + /// <summary> + /// Returns true if the Rect2 is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the rect has area.</returns> public bool HasNoArea() { return _size.x <= 0 || _size.y <= 0; } + /// <summary> + /// Returns true if the Rect2 contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the rect contains `point`.</returns> public bool HasPoint(Vector2 point) { if (point.x < _position.x) @@ -155,20 +239,65 @@ namespace Godot return true; } - public bool Intersects(Rect2 b) + /// <summary> + /// Returns true if the Rect2 overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="b">The other rect to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(Rect2 b, bool includeBorders = false) { - if (_position.x >= b._position.x + b._size.x) - return false; - if (_position.x + _size.x <= b._position.x) - return false; - if (_position.y >= b._position.y + b._size.y) - return false; - if (_position.y + _size.y <= b._position.y) - return false; + if (includeBorders) + { + if (_position.x > b._position.x + b._size.x) + { + return false; + } + if (_position.x + _size.x < b._position.x) + { + return false; + } + if (_position.y > b._position.y + b._size.y) + { + return false; + } + if (_position.y + _size.y < b._position.y) + { + return false; + } + } + else + { + if (_position.x >= b._position.x + b._size.x) + { + return false; + } + if (_position.x + _size.x <= b._position.x) + { + return false; + } + if (_position.y >= b._position.y + b._size.y) + { + return false; + } + if (_position.y + _size.y <= b._position.y) + { + return false; + } + } return true; } + /// <summary> + /// Returns a larger Rect2 that contains this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The merged rect.</returns> public Rect2 Merge(Rect2 b) { Rect2 newRect; @@ -179,27 +308,53 @@ namespace Godot newRect._size.x = Mathf.Max(b._position.x + b._size.x, _position.x + _size.x); newRect._size.y = Mathf.Max(b._position.y + b._size.y, _position.y + _size.y); - newRect._size = newRect._size - newRect._position; // Make relative again + newRect._size -= newRect._position; // Make relative again return newRect; } - // Constructors + /// <summary> + /// Constructs a Rect2 from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size.</param> public Rect2(Vector2 position, Vector2 size) { _position = position; _size = size; } + + /// <summary> + /// Constructs a Rect2 from a position, width, and height. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2(Vector2 position, real_t width, real_t height) { _position = position; _size = new Vector2(width, height); } + + /// <summary> + /// Constructs a Rect2 from x, y, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="size">The size.</param> public Rect2(real_t x, real_t y, Vector2 size) { _position = new Vector2(x, y); _size = size; } + + /// <summary> + /// Constructs a Rect2 from x, y, width, and height. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2(real_t x, real_t y, real_t width, real_t height) { _position = new Vector2(x, y); @@ -231,6 +386,12 @@ namespace Godot return _position.Equals(other._position) && _size.Equals(other._size); } + /// <summary> + /// Returns true if this rect and `other` are approximately equal, by running + /// <see cref="Vector2.IsEqualApprox(Vector2)"/> on each component. + /// </summary> + /// <param name="other">The other rect to compare.</param> + /// <returns>Whether or not the rects are approximately equal.</returns> public bool IsEqualApprox(Rect2 other) { return _position.IsEqualApprox(other._position) && _size.IsEqualApprox(other.Size); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs index c0b236c524..8f71c00d76 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2i.cs @@ -3,6 +3,10 @@ using System.Runtime.InteropServices; namespace Godot { + /// <summary> + /// 2D axis-aligned bounding box using integers. Rect2i consists of a position, a size, and + /// several utility functions. It is typically used for fast overlap tests. + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Rect2i : IEquatable<Rect2i> @@ -10,29 +14,52 @@ namespace Godot private Vector2i _position; private Vector2i _size; + /// <summary> + /// Beginning corner. Typically has values lower than End. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2i Position { get { return _position; } set { _position = value; } } + /// <summary> + /// Size from Position to End. Typically all components are positive. + /// If the size is negative, you can use <see cref="Abs"/> to fix it. + /// </summary> + /// <value>Directly uses a private field.</value> public Vector2i Size { get { return _size; } set { _size = value; } } + /// <summary> + /// Ending corner. This is calculated as <see cref="Position"/> plus + /// <see cref="Size"/>. Setting this value will change the size. + /// </summary> + /// <value>Getting is equivalent to `value = Position + Size`, setting is equivalent to `Size = value - Position`.</value> public Vector2i End { get { return _position + _size; } set { _size = value - _position; } } + /// <summary> + /// The area of this rect. + /// </summary> + /// <value>Equivalent to <see cref="GetArea()"/>.</value> public int Area { get { return GetArea(); } } + /// <summary> + /// Returns a Rect2i with equivalent position and size, modified so that + /// the top-left corner is the origin and width and height are positive. + /// </summary> + /// <returns>The modified rect.</returns> public Rect2i Abs() { Vector2i end = End; @@ -40,12 +67,19 @@ namespace Godot return new Rect2i(topLeft, _size.Abs()); } + /// <summary> + /// Returns the intersection of this Rect2i and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The clipped rect.</returns> public Rect2i Clip(Rect2i b) { var newRect = b; if (!Intersects(newRect)) + { return new Rect2i(); + } newRect._position.x = Mathf.Max(b._position.x, _position.x); newRect._position.y = Mathf.Max(b._position.y, _position.y); @@ -59,6 +93,11 @@ namespace Godot return newRect; } + /// <summary> + /// Returns true if this Rect2i completely encloses another one. + /// </summary> + /// <param name="b">The other rect that may be enclosed.</param> + /// <returns>A bool for whether or not this rect encloses `b`.</returns> public bool Encloses(Rect2i b) { return b._position.x >= _position.x && b._position.y >= _position.y && @@ -66,6 +105,11 @@ namespace Godot b._position.y + b._size.y < _position.y + _size.y; } + /// <summary> + /// Returns this Rect2i expanded to include a given point. + /// </summary> + /// <param name="to">The point to include.</param> + /// <returns>The expanded rect.</returns> public Rect2i Expand(Vector2i to) { var expanded = this; @@ -74,14 +118,22 @@ namespace Godot Vector2i end = expanded._position + expanded._size; if (to.x < begin.x) + { begin.x = to.x; + } if (to.y < begin.y) + { begin.y = to.y; + } if (to.x > end.x) + { end.x = to.x; + } if (to.y > end.y) + { end.y = to.y; + } expanded._position = begin; expanded._size = end - begin; @@ -89,11 +141,20 @@ namespace Godot return expanded; } + /// <summary> + /// Returns the area of the Rect2. + /// </summary> + /// <returns>The area.</returns> public int GetArea() { return _size.x * _size.y; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards all the sides. + /// </summary> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2i Grow(int by) { var g = this; @@ -106,6 +167,14 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards each direction individually. + /// </summary> + /// <param name="left">The amount to grow by on the left.</param> + /// <param name="top">The amount to grow by on the top.</param> + /// <param name="right">The amount to grow by on the right.</param> + /// <param name="bottom">The amount to grow by on the bottom.</param> + /// <returns>The grown rect.</returns> public Rect2i GrowIndividual(int left, int top, int right, int bottom) { var g = this; @@ -118,6 +187,12 @@ namespace Godot return g; } + /// <summary> + /// Returns a copy of the Rect2i grown a given amount of units towards the <see cref="Margin"/> direction. + /// </summary> + /// <param name="margin">The direction to grow in.</param> + /// <param name="by">The amount to grow by.</param> + /// <returns>The grown rect.</returns> public Rect2i GrowMargin(Margin margin, int by) { var g = this; @@ -130,11 +205,20 @@ namespace Godot return g; } + /// <summary> + /// Returns true if the Rect2 is flat or empty, or false otherwise. + /// </summary> + /// <returns>A bool for whether or not the rect has area.</returns> public bool HasNoArea() { return _size.x <= 0 || _size.y <= 0; } + /// <summary> + /// Returns true if the Rect2 contains a point, or false otherwise. + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns>A bool for whether or not the rect contains `point`.</returns> public bool HasPoint(Vector2i point) { if (point.x < _position.x) @@ -150,20 +234,49 @@ namespace Godot return true; } - public bool Intersects(Rect2i b) + /// <summary> + /// Returns true if the Rect2i overlaps with `b` + /// (i.e. they have at least one point in common). + /// + /// If `includeBorders` is true, they will also be considered overlapping + /// if their borders touch, even without intersection. + /// </summary> + /// <param name="b">The other rect to check for intersections with.</param> + /// <param name="includeBorders">Whether or not to consider borders.</param> + /// <returns>A bool for whether or not they are intersecting.</returns> + public bool Intersects(Rect2i b, bool includeBorders = false) { - if (_position.x >= b._position.x + b._size.x) - return false; - if (_position.x + _size.x <= b._position.x) - return false; - if (_position.y >= b._position.y + b._size.y) - return false; - if (_position.y + _size.y <= b._position.y) - return false; + if (includeBorders) + { + if (_position.x > b._position.x + b._size.x) + return false; + if (_position.x + _size.x < b._position.x) + return false; + if (_position.y > b._position.y + b._size.y) + return false; + if (_position.y + _size.y < b._position.y) + return false; + } + else + { + if (_position.x >= b._position.x + b._size.x) + return false; + if (_position.x + _size.x <= b._position.x) + return false; + if (_position.y >= b._position.y + b._size.y) + return false; + if (_position.y + _size.y <= b._position.y) + return false; + } return true; } + /// <summary> + /// Returns a larger Rect2i that contains this Rect2 and `b`. + /// </summary> + /// <param name="b">The other rect.</param> + /// <returns>The merged rect.</returns> public Rect2i Merge(Rect2i b) { Rect2i newRect; @@ -174,27 +287,53 @@ namespace Godot newRect._size.x = Mathf.Max(b._position.x + b._size.x, _position.x + _size.x); newRect._size.y = Mathf.Max(b._position.y + b._size.y, _position.y + _size.y); - newRect._size = newRect._size - newRect._position; // Make relative again + newRect._size -= newRect._position; // Make relative again return newRect; } - // Constructors + /// <summary> + /// Constructs a Rect2i from a position and size. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="size">The size.</param> public Rect2i(Vector2i position, Vector2i size) { _position = position; _size = size; } + + /// <summary> + /// Constructs a Rect2i from a position, width, and height. + /// </summary> + /// <param name="position">The position.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2i(Vector2i position, int width, int height) { _position = position; _size = new Vector2i(width, height); } + + /// <summary> + /// Constructs a Rect2i from x, y, and size. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="size">The size.</param> public Rect2i(int x, int y, Vector2i size) { _position = new Vector2i(x, y); _size = size; } + + /// <summary> + /// Constructs a Rect2i from x, y, width, and height. + /// </summary> + /// <param name="x">The position's X coordinate.</param> + /// <param name="y">The position's Y coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> public Rect2i(int x, int y, int width, int height) { _position = new Vector2i(x, y); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs index 6a58b90561..ac47f6029f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform.cs @@ -8,11 +8,28 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 3×4 matrix (3 rows, 4 columns) used for 3D linear transformations. + /// It can represent transformations such as translation, rotation, or scaling. + /// It consists of a <see cref="Basis"/> (first 3 columns) and a + /// <see cref="Vector3"/> for the origin (last column). + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Transform : IEquatable<Transform> { + /// <summary> + /// The <see cref="Basis"/> of this transform. Contains the X, Y, and Z basis + /// vectors (columns 0 to 2) and is responsible for rotation and scale. + /// </summary> public Basis basis; + + /// <summary> + /// The origin vector (column 3, the fourth column). Equivalent to array index `[3]`. + /// </summary> public Vector3 origin; /// <summary> @@ -85,13 +102,24 @@ namespace Godot } } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation, scaling, and translation. + /// </summary> + /// <returns>The inverse transformation matrix.</returns> public Transform AffineInverse() { Basis basisInv = basis.Inverse(); return new Transform(basisInv, basisInv.Xform(-origin)); } - public Transform InterpolateWith(Transform transform, real_t c) + /// <summary> + /// Interpolates this transform to the other `transform` by `weight`. + /// </summary> + /// <param name="transform">The other transform.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated transform.</returns> + public Transform InterpolateWith(Transform transform, real_t weight) { /* not sure if very "efficient" but good enough? */ @@ -104,18 +132,37 @@ namespace Godot Vector3 destinationLocation = transform.origin; var interpolated = new Transform(); - interpolated.basis.SetQuatScale(sourceRotation.Slerp(destinationRotation, c).Normalized(), sourceScale.Lerp(destinationScale, c)); - interpolated.origin = sourceLocation.Lerp(destinationLocation, c); + interpolated.basis.SetQuatScale(sourceRotation.Slerp(destinationRotation, weight).Normalized(), sourceScale.Lerp(destinationScale, weight)); + interpolated.origin = sourceLocation.Lerp(destinationLocation, weight); return interpolated; } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation and translation + /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// </summary> + /// <returns>The inverse matrix.</returns> public Transform Inverse() { Basis basisTr = basis.Transposed(); return new Transform(basisTr, basisTr.Xform(-origin)); } + /// <summary> + /// Returns a copy of the transform rotated such that its + /// -Z axis (forward) points towards the target position. + /// + /// The transform will first be rotated around the given up vector, + /// and then fully aligned to the target by a further rotation around + /// an axis perpendicular to both the target and up vectors. + /// + /// Operations take place in global space. + /// </summary> + /// <param name="target">The object to look at.</param> + /// <param name="up">The relative up direction</param> + /// <returns>The resulting transform.</returns> public Transform LookingAt(Vector3 target, Vector3 up) { var t = this; @@ -123,22 +170,39 @@ namespace Godot return t; } + /// <summary> + /// Returns the transform with the basis orthogonal (90 degrees), + /// and normalized axis vectors (scale of 1 or -1). + /// </summary> + /// <returns>The orthonormalized transform.</returns> public Transform Orthonormalized() { return new Transform(basis.Orthonormalized(), origin); } + /// <summary> + /// Rotates the transform around the given `axis` by `phi` (in radians), + /// using matrix multiplication. The axis must be a normalized vector. + /// </summary> + /// <param name="axis">The axis to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated transformation matrix.</returns> public Transform Rotated(Vector3 axis, real_t phi) { return new Transform(new Basis(axis, phi), new Vector3()) * this; } + /// <summary> + /// Scales the transform by the given 3D scaling factor, using matrix multiplication. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled transformation matrix.</returns> public Transform Scaled(Vector3 scale) { return new Transform(basis.Scaled(scale), origin * scale); } - public void SetLookAt(Vector3 eye, Vector3 target, Vector3 up) + private void SetLookAt(Vector3 eye, Vector3 target, Vector3 up) { // Make rotation matrix // Z vector @@ -161,16 +225,30 @@ namespace Godot origin = eye; } - public Transform Translated(Vector3 ofs) + /// <summary> + /// Translates the transform by the given `offset`, + /// relative to the transform's basis vectors. + /// + /// Unlike <see cref="Rotated"/> and <see cref="Scaled"/>, + /// this does not use matrix multiplication. + /// </summary> + /// <param name="offset">The offset to translate by.</param> + /// <returns>The translated matrix.</returns> + public Transform Translated(Vector3 offset) { return new Transform(basis, new Vector3 ( - origin[0] += basis.Row0.Dot(ofs), - origin[1] += basis.Row1.Dot(ofs), - origin[2] += basis.Row2.Dot(ofs) + origin[0] += basis.Row0.Dot(offset), + origin[1] += basis.Row1.Dot(offset), + origin[2] += basis.Row2.Dot(offset) )); } + /// <summary> + /// Returns a vector transformed (multiplied) by this transformation matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector3 Xform(Vector3 v) { return new Vector3 @@ -181,6 +259,14 @@ namespace Godot ); } + /// <summary> + /// Returns a vector transformed (multiplied) by the transposed transformation matrix. + /// + /// Note: This results in a multiplication by the inverse of the + /// transformation matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector3 XformInv(Vector3 v) { Vector3 vInv = v - origin; @@ -199,24 +285,58 @@ namespace Godot private static readonly Transform _flipY = new Transform(new Basis(1, 0, 0, 0, -1, 0, 0, 0, 1), Vector3.Zero); private static readonly Transform _flipZ = new Transform(new Basis(1, 0, 0, 0, 1, 0, 0, 0, -1), Vector3.Zero); + /// <summary> + /// The identity transform, with no translation, rotation, or scaling applied. + /// This is used as a replacement for `Transform()` in GDScript. + /// Do not use `new Transform()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Up, Vector3.Back, Vector3.Zero)`.</value> public static Transform Identity { get { return _identity; } } + /// <summary> + /// The transform that will flip something along the X axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Left, Vector3.Up, Vector3.Back, Vector3.Zero)`.</value> public static Transform FlipX { get { return _flipX; } } + /// <summary> + /// The transform that will flip something along the Y axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Down, Vector3.Back, Vector3.Zero)`.</value> public static Transform FlipY { get { return _flipY; } } + /// <summary> + /// The transform that will flip something along the Z axis. + /// </summary> + /// <value>Equivalent to `new Transform(Vector3.Right, Vector3.Up, Vector3.Forward, Vector3.Zero)`.</value> public static Transform FlipZ { get { return _flipZ; } } - // Constructors + /// <summary> + /// Constructs a transformation matrix from 4 vectors (matrix columns). + /// </summary> + /// <param name="column0">The X vector, or column index 0.</param> + /// <param name="column1">The Y vector, or column index 1.</param> + /// <param name="column2">The Z vector, or column index 2.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Vector3 column0, Vector3 column1, Vector3 column2, Vector3 origin) { basis = new Basis(column0, column1, column2); this.origin = origin; } + /// <summary> + /// Constructs a transformation matrix from the given quaternion and origin vector. + /// </summary> + /// <param name="quat">The <see cref="Godot.Quat"/> to create the basis from.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Quat quat, Vector3 origin) { basis = new Basis(quat); this.origin = origin; } + /// <summary> + /// Constructs a transformation matrix from the given basis and origin vector. + /// </summary> + /// <param name="basis">The <see cref="Godot.Basis"/> to create the basis from.</param> + /// <param name="origin">The origin vector, or column index 3.</param> public Transform(Basis basis, Vector3 origin) { this.basis = basis; @@ -255,6 +375,12 @@ namespace Godot return basis.Equals(other.basis) && origin.Equals(other.origin); } + /// <summary> + /// Returns true if this transform and `other` are approximately equal, by running + /// <see cref="Vector3.IsEqualApprox(Vector3)"/> on each component. + /// </summary> + /// <param name="other">The other transform to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Transform other) { return basis.IsEqualApprox(other.basis) && origin.IsEqualApprox(other.origin); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs index 3ae96d4922..06bbe98497 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs @@ -8,25 +8,44 @@ using real_t = System.Single; namespace Godot { + /// <summary> + /// 2×3 matrix (2 rows, 3 columns) used for 2D linear transformations. + /// It can represent transformations such as translation, rotation, or scaling. + /// It consists of a three <see cref="Vector2"/> values: x, y, and the origin. + /// + /// For more information, read this documentation article: + /// https://docs.godotengine.org/en/latest/tutorials/math/matrices_and_transforms.html + /// </summary> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Transform2D : IEquatable<Transform2D> { + /// <summary> + /// The basis matrix's X vector (column 0). Equivalent to array index `[0]`. + /// </summary> + /// <value></value> public Vector2 x; + + /// <summary> + /// The basis matrix's Y vector (column 1). Equivalent to array index `[1]`. + /// </summary> public Vector2 y; + + /// <summary> + /// The origin vector (column 2, the third column). Equivalent to array index `[2]`. + /// The origin vector represents translation. + /// </summary> public Vector2 origin; + /// <summary> + /// The rotation of this transformation matrix. + /// </summary> + /// <value>Getting is equivalent to calling <see cref="Mathf.Atan2(real_t, real_t)"/> with the values of <see cref="x"/>.</value> public real_t Rotation { get { - real_t det = BasisDeterminant(); - Transform2D t = Orthonormalized(); - if (det < 0) - { - t.ScaleBasis(new Vector2(1, -1)); - } - return Mathf.Atan2(t.x.y, t.x.x); + return Mathf.Atan2(x.y, x.x); } set { @@ -38,6 +57,10 @@ namespace Godot } } + /// <summary> + /// The scale of this transformation matrix. + /// </summary> + /// <value>Equivalent to the lengths of each column vector, but Y is negative if the determinant is negative.</value> public Vector2 Scale { get @@ -47,8 +70,7 @@ namespace Godot } set { - x = x.Normalized(); - y = y.Normalized(); + value /= Scale; // Value becomes what's called "delta_scale" in core. x *= value.x; y *= value.y; } @@ -112,6 +134,11 @@ namespace Godot } } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation, scaling, and translation. + /// </summary> + /// <returns>The inverse transformation matrix.</returns> public Transform2D AffineInverse() { real_t det = BasisDeterminant(); @@ -135,28 +162,58 @@ namespace Godot return inv; } + /// <summary> + /// Returns the determinant of the basis matrix. If the basis is + /// uniformly scaled, its determinant is the square of the scale. + /// + /// A negative determinant means the Y scale is negative. + /// A zero determinant means the basis isn't invertible, + /// and is usually considered invalid. + /// </summary> + /// <returns>The determinant of the basis matrix.</returns> private real_t BasisDeterminant() { return x.x * y.y - x.y * y.x; } + /// <summary> + /// Returns a vector transformed (multiplied) by the basis matrix. + /// This method does not account for translation (the origin vector). + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector2 BasisXform(Vector2 v) { return new Vector2(Tdotx(v), Tdoty(v)); } + /// <summary> + /// Returns a vector transformed (multiplied) by the inverse basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// Note: This results in a multiplication by the inverse of the + /// basis matrix only if it represents a rotation-reflection. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector2 BasisXformInv(Vector2 v) { return new Vector2(x.Dot(v), y.Dot(v)); } - public Transform2D InterpolateWith(Transform2D m, real_t c) + /// <summary> + /// Interpolates this transform to the other `transform` by `weight`. + /// </summary> + /// <param name="transform">The other transform.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated transform.</returns> + public Transform2D InterpolateWith(Transform2D transform, real_t weight) { real_t r1 = Rotation; - real_t r2 = m.Rotation; + real_t r2 = transform.Rotation; Vector2 s1 = Scale; - Vector2 s2 = m.Scale; + Vector2 s2 = transform.Scale; // Slerp rotation var v1 = new Vector2(Mathf.Cos(r1), Mathf.Sin(r1)); @@ -172,28 +229,34 @@ namespace Godot if (dot > 0.9995f) { // Linearly interpolate to avoid numerical precision issues - v = v1.Lerp(v2, c).Normalized(); + v = v1.Lerp(v2, weight).Normalized(); } else { - real_t angle = c * Mathf.Acos(dot); + real_t angle = weight * Mathf.Acos(dot); Vector2 v3 = (v2 - v1 * dot).Normalized(); v = v1 * Mathf.Cos(angle) + v3 * Mathf.Sin(angle); } // Extract parameters Vector2 p1 = origin; - Vector2 p2 = m.origin; + Vector2 p2 = transform.origin; // Construct matrix - var res = new Transform2D(Mathf.Atan2(v.y, v.x), p1.Lerp(p2, c)); - Vector2 scale = s1.Lerp(s2, c); + var res = new Transform2D(Mathf.Atan2(v.y, v.x), p1.Lerp(p2, weight)); + Vector2 scale = s1.Lerp(s2, weight); res.x *= scale; res.y *= scale; return res; } + /// <summary> + /// Returns the inverse of the transform, under the assumption that + /// the transformation is composed of rotation and translation + /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// </summary> + /// <returns>The inverse matrix.</returns> public Transform2D Inverse() { var inv = this; @@ -208,6 +271,11 @@ namespace Godot return inv; } + /// <summary> + /// Returns the transform with the basis orthogonal (90 degrees), + /// and normalized axis vectors (scale of 1 or -1). + /// </summary> + /// <returns>The orthonormalized transform.</returns> public Transform2D Orthonormalized() { var on = this; @@ -225,11 +293,21 @@ namespace Godot return on; } + /// <summary> + /// Rotates the transform by `phi` (in radians), using matrix multiplication. + /// </summary> + /// <param name="phi">The angle to rotate, in radians.</param> + /// <returns>The rotated transformation matrix.</returns> public Transform2D Rotated(real_t phi) { return this * new Transform2D(phi, new Vector2()); } + /// <summary> + /// Scales the transform by the given scaling factor, using matrix multiplication. + /// </summary> + /// <param name="scale">The scale to introduce.</param> + /// <returns>The scaled transformation matrix.</returns> public Transform2D Scaled(Vector2 scale) { var copy = this; @@ -257,6 +335,15 @@ namespace Godot return this[0, 1] * with[0] + this[1, 1] * with[1]; } + /// <summary> + /// Translates the transform by the given `offset`, + /// relative to the transform's basis vectors. + /// + /// Unlike <see cref="Rotated"/> and <see cref="Scaled"/>, + /// this does not use matrix multiplication. + /// </summary> + /// <param name="offset">The offset to translate by.</param> + /// <returns>The translated matrix.</returns> public Transform2D Translated(Vector2 offset) { var copy = this; @@ -264,11 +351,21 @@ namespace Godot return copy; } + /// <summary> + /// Returns a vector transformed (multiplied) by this transformation matrix. + /// </summary> + /// <param name="v">A vector to transform.</param> + /// <returns>The transformed vector.</returns> public Vector2 Xform(Vector2 v) { return new Vector2(Tdotx(v), Tdoty(v)) + origin; } + /// <summary> + /// Returns a vector transformed (multiplied) by the inverse transformation matrix. + /// </summary> + /// <param name="v">A vector to inversely transform.</param> + /// <returns>The inversely transformed vector.</returns> public Vector2 XformInv(Vector2 v) { Vector2 vInv = v - origin; @@ -280,11 +377,30 @@ namespace Godot private static readonly Transform2D _flipX = new Transform2D(-1, 0, 0, 1, 0, 0); private static readonly Transform2D _flipY = new Transform2D(1, 0, 0, -1, 0, 0); - public static Transform2D Identity => _identity; - public static Transform2D FlipX => _flipX; - public static Transform2D FlipY => _flipY; + /// <summary> + /// The identity transform, with no translation, rotation, or scaling applied. + /// This is used as a replacement for `Transform2D()` in GDScript. + /// Do not use `new Transform2D()` with no arguments in C#, because it sets all values to zero. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Right, Vector2.Down, Vector2.Zero)`.</value> + public static Transform2D Identity { get { return _identity; } } + /// <summary> + /// The transform that will flip something along the X axis. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Left, Vector2.Down, Vector2.Zero)`.</value> + public static Transform2D FlipX { get { return _flipX; } } + /// <summary> + /// The transform that will flip something along the Y axis. + /// </summary> + /// <value>Equivalent to `new Transform2D(Vector2.Right, Vector2.Up, Vector2.Zero)`.</value> + public static Transform2D FlipY { get { return _flipY; } } - // Constructors + /// <summary> + /// Constructs a transformation matrix from 3 vectors (matrix columns). + /// </summary> + /// <param name="xAxis">The X vector, or column index 0.</param> + /// <param name="yAxis">The Y vector, or column index 1.</param> + /// <param name="originPos">The origin vector, or column index 2.</param> public Transform2D(Vector2 xAxis, Vector2 yAxis, Vector2 originPos) { x = xAxis; @@ -292,7 +408,16 @@ namespace Godot origin = originPos; } - // Arguments are named such that xy is equal to calling x.y + /// <summary> + /// Constructs a transformation matrix from the given components. + /// Arguments are named such that xy is equal to calling x.y + /// </summary> + /// <param name="xx">The X component of the X column vector, accessed via `t.x.x` or `[0][0]`</param> + /// <param name="xy">The Y component of the X column vector, accessed via `t.x.y` or `[0][1]`</param> + /// <param name="yx">The X component of the Y column vector, accessed via `t.y.x` or `[1][0]`</param> + /// <param name="yy">The Y component of the Y column vector, accessed via `t.y.y` or `[1][1]`</param> + /// <param name="ox">The X component of the origin vector, accessed via `t.origin.x` or `[2][0]`</param> + /// <param name="oy">The Y component of the origin vector, accessed via `t.origin.y` or `[2][1]`</param> public Transform2D(real_t xx, real_t xy, real_t yx, real_t yy, real_t ox, real_t oy) { x = new Vector2(xx, xy); @@ -300,6 +425,11 @@ namespace Godot origin = new Vector2(ox, oy); } + /// <summary> + /// Constructs a transformation matrix from a rotation value and origin vector. + /// </summary> + /// <param name="rot">The rotation of the new transform, in radians.</param> + /// <param name="pos">The origin vector, or column index 2.</param> public Transform2D(real_t rot, Vector2 pos) { x.x = y.y = Mathf.Cos(rot); @@ -345,6 +475,12 @@ namespace Godot return x.Equals(other.x) && y.Equals(other.y) && origin.Equals(other.origin); } + /// <summary> + /// Returns true if this transform and `other` are approximately equal, by running + /// <see cref="Vector2.IsEqualApprox(Vector2)"/> on each component. + /// </summary> + /// <param name="other">The other transform to compare.</param> + /// <returns>Whether or not the matrices are approximately equal.</returns> public bool IsEqualApprox(Transform2D other) { return x.IsEqualApprox(other.x) && y.IsEqualApprox(other.y) && origin.IsEqualApprox(other.origin); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index 7e4804f9fd..26bd828a5b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -21,15 +21,29 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector2 : IEquatable<Vector2> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, Y } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public real_t x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public real_t y; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`.</value> public real_t this[int index] { get @@ -76,41 +90,80 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(real_t)"/> called on each component.</returns> public Vector2 Abs() { return new Vector2(Mathf.Abs(x), Mathf.Abs(y)); } + /// <summary> + /// Returns this vector's angle with respect to the X axis, or (1, 0) vector, in radians. + /// + /// Equivalent to the result of <see cref="Mathf.Atan2(real_t, real_t)"/> when + /// called with the vector's `y` and `x` as parameters: `Mathf.Atan2(v.y, v.x)`. + /// </summary> + /// <returns>The angle of this vector, in radians.</returns> public real_t Angle() { return Mathf.Atan2(y, x); } + /// <summary> + /// Returns the angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector2 to) { return Mathf.Atan2(Cross(to), Dot(to)); } + /// <summary> + /// Returns the angle between the line connecting the two points and the X axis, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleToPoint(Vector2 to) { return Mathf.Atan2(y - to.y, x - to.x); } + /// <summary> + /// Returns the aspect ratio of this vector, the ratio of `x` to `y`. + /// </summary> + /// <returns>The `x` component divided by the `y` component.</returns> public real_t Aspect() { return x / y; } - public Vector2 Bounce(Vector2 n) + /// <summary> + /// Returns the vector "bounced off" from a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to bounce off. Must be normalized.</param> + /// <returns>The bounced vector.</returns> + public Vector2 Bounce(Vector2 normal) { - return -Reflect(n); + return -Reflect(normal); } + /// <summary> + /// Returns a new vector with all components rounded up (towards positive infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Ceil"/> called on each component.</returns> public Vector2 Ceil() { return new Vector2(Mathf.Ceil(x), Mathf.Ceil(y)); } + /// <summary> + /// Returns the vector with a maximum length by limiting its length to `length`. + /// </summary> + /// <param name="length">The length to limit to.</param> + /// <returns>The vector with its length limited.</returns> public Vector2 Clamped(real_t length) { var v = this; @@ -125,17 +178,30 @@ namespace Godot return v; } + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product value.</returns> public real_t Cross(Vector2 b) { return x * b.y - y * b.x; } + /// <summary> + /// Performs a cubic interpolation between vectors `preA`, this vector, `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination vector.</param> + /// <param name="preA">A vector before this vector.</param> + /// <param name="postB">A vector after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated vector.</returns> public Vector2 CubicInterpolate(Vector2 b, Vector2 preA, Vector2 postB, real_t t) { - var p0 = preA; - var p1 = this; - var p2 = b; - var p3 = postB; + Vector2 p0 = preA; + Vector2 p1 = this; + Vector2 p2 = b; + Vector2 p3 = postB; real_t t2 = t * t; real_t t3 = t2 * t; @@ -146,46 +212,102 @@ namespace Godot (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3); } + /// <summary> + /// Returns the normalized vector pointing from this vector to `b`. + /// </summary> + /// <param name="b">The other vector to point towards.</param> + /// <returns>The direction from this vector to `b`.</returns> public Vector2 DirectionTo(Vector2 b) { return new Vector2(b.x - x, b.y - y).Normalized(); } + /// <summary> + /// Returns the squared distance between this vector and `to`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public real_t DistanceSquaredTo(Vector2 to) { return (x - to.x) * (x - to.x) + (y - to.y) * (y - to.y); } + /// <summary> + /// Returns the distance between this vector and `to`. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector2 to) { return Mathf.Sqrt((x - to.x) * (x - to.x) + (y - to.y) * (y - to.y)); } + /// <summary> + /// Returns the dot product of this vector and `with`. + /// </summary> + /// <param name="with">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public real_t Dot(Vector2 with) { return x * with.x + y * with.y; } + /// <summary> + /// Returns a new vector with all components rounded down (towards negative infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Floor"/> called on each component.</returns> public Vector2 Floor() { return new Vector2(Mathf.Floor(x), Mathf.Floor(y)); } + /// <summary> + /// Returns the inverse of this vector. This is the same as `new Vector2(1 / v.x, 1 / v.y)`. + /// </summary> + /// <returns>The inverse of this vector.</returns> + public Vector2 Inverse() + { + return new Vector2(1 / x, 1 / y); + } + + /// <summary> + /// Returns true if the vector is normalized, and false otherwise. + /// </summary> + /// <returns>A bool indicating whether or not the vector is normalized.</returns> public bool IsNormalized() { return Mathf.Abs(LengthSquared() - 1.0f) < Mathf.Epsilon; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { return Mathf.Sqrt(x * x + y * y); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public real_t LengthSquared() { return x * x + y * y; } + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> public Vector2 Lerp(Vector2 to, real_t weight) { return new Vector2 @@ -195,6 +317,13 @@ namespace Godot ); } + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by the vector amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A vector with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> public Vector2 Lerp(Vector2 to, Vector2 weight) { return new Vector2 @@ -204,6 +333,32 @@ namespace Godot ); } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> + public Axis MaxAxis() + { + return x < y ? Axis.Y : Axis.X; + } + + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.Y"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> + public Axis MinAxis() + { + return x < y ? Axis.X : Axis.Y; + } + + /// <summary> + /// Moves this vector toward `to` by the fixed `delta` amount. + /// </summary> + /// <param name="to">The vector to move towards.</param> + /// <param name="delta">The amount to move towards by.</param> + /// <returns>The resulting vector.</returns> public Vector2 MoveToward(Vector2 to, real_t delta) { var v = this; @@ -212,6 +367,10 @@ namespace Godot return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; } + /// <summary> + /// Returns the vector scaled to unit length. Equivalent to `v / v.Length()`. + /// </summary> + /// <returns>A normalized version of the vector.</returns> public Vector2 Normalized() { var v = this; @@ -219,6 +378,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `mod`.</returns> public Vector2 PosMod(real_t mod) { Vector2 v; @@ -227,6 +391,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `modv`'s components.</returns> public Vector2 PosMod(Vector2 modv) { Vector2 v; @@ -235,27 +404,59 @@ namespace Godot return v; } + /// <summary> + /// Returns this vector projected onto another vector `b`. + /// </summary> + /// <param name="onNormal">The vector to project onto.</param> + /// <returns>The projected vector.</returns> public Vector2 Project(Vector2 onNormal) { return onNormal * (Dot(onNormal) / onNormal.LengthSquared()); } - public Vector2 Reflect(Vector2 n) + /// <summary> + /// Returns this vector reflected from a plane defined by the given `normal`. + /// </summary> + /// <param name="normal">The normal vector defining the plane to reflect from. Must be normalized.</param> + /// <returns>The reflected vector.</returns> + public Vector2 Reflect(Vector2 normal) { - return 2 * Dot(n) * n - this; +#if DEBUG + if (!normal.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(normal)); + } +#endif + return 2 * Dot(normal) * normal - this; } + /// <summary> + /// Rotates this vector by `phi` radians. + /// </summary> + /// <param name="phi">The angle to rotate by, in radians.</param> + /// <returns>The rotated vector.</returns> public Vector2 Rotated(real_t phi) { real_t rads = Angle() + phi; return new Vector2(Mathf.Cos(rads), Mathf.Sin(rads)) * Length(); } + /// <summary> + /// Returns this vector with all components rounded to the nearest integer, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <returns>The rounded vector.</returns> public Vector2 Round() { return new Vector2(Mathf.Round(x), Mathf.Round(y)); } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(real_t)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector2 Sign() { Vector2 v; @@ -264,23 +465,57 @@ namespace Godot return v; } - public Vector2 Slerp(Vector2 b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this vector and `to` by amount `weight`. + /// + /// Note: Both vectors must be normalized. + /// </summary> + /// <param name="to">The destination vector for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector2 Slerp(Vector2 to, real_t weight) { - real_t theta = AngleTo(b); - return Rotated(theta * t); +#if DEBUG + if (!IsNormalized()) + { + throw new InvalidOperationException("Vector2.Slerp: From vector is not normalized."); + } + if (!to.IsNormalized()) + { + throw new InvalidOperationException("Vector2.Slerp: `to` is not normalized."); + } +#endif + return Rotated(AngleTo(to) * weight); } - public Vector2 Slide(Vector2 n) + /// <summary> + /// Returns this vector slid along a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <returns>The slid vector.</returns> + public Vector2 Slide(Vector2 normal) { - return this - n * Dot(n); + return this - normal * Dot(normal); } - public Vector2 Snapped(Vector2 by) + /// <summary> + /// Returns this vector with each component snapped to the nearest multiple of `step`. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public Vector2 Snapped(Vector2 step) { - return new Vector2(Mathf.Stepify(x, by.x), Mathf.Stepify(y, by.y)); + return new Vector2(Mathf.Stepify(x, step.x), Mathf.Stepify(y, step.y)); } - public Vector2 Tangent() + /// <summary> + /// Returns a perpendicular vector rotated 90 degrees counter-clockwise + /// compared to the original, with the same length. + /// </summary> + /// <returns>The perpendicular vector.</returns> + public Vector2 Perpendicular() { return new Vector2(y, -x); } @@ -288,7 +523,6 @@ namespace Godot // Constants private static readonly Vector2 _zero = new Vector2(0, 0); private static readonly Vector2 _one = new Vector2(1, 1); - private static readonly Vector2 _negOne = new Vector2(-1, -1); private static readonly Vector2 _inf = new Vector2(Mathf.Inf, Mathf.Inf); private static readonly Vector2 _up = new Vector2(0, -1); @@ -296,22 +530,58 @@ namespace Godot private static readonly Vector2 _right = new Vector2(1, 0); private static readonly Vector2 _left = new Vector2(-1, 0); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector2(0, 0)`</value> public static Vector2 Zero { get { return _zero; } } - public static Vector2 NegOne { get { return _negOne; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector2(1, 1)`</value> public static Vector2 One { get { return _one; } } + /// <summary> + /// Infinity vector, a vector with all components set to `Mathf.Inf`. + /// </summary> + /// <value>Equivalent to `new Vector2(Mathf.Inf, Mathf.Inf)`</value> public static Vector2 Inf { get { return _inf; } } + /// <summary> + /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// </summary> + /// <value>Equivalent to `new Vector2(0, -1)`</value> public static Vector2 Up { get { return _up; } } + /// <summary> + /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// </summary> + /// <value>Equivalent to `new Vector2(0, 1)`</value> public static Vector2 Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the direction of right. + /// </summary> + /// <value>Equivalent to `new Vector2(1, 0)`</value> public static Vector2 Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the direction of left. + /// </summary> + /// <value>Equivalent to `new Vector2(-1, 0)`</value> public static Vector2 Left { get { return _left; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector2"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> public Vector2(real_t x, real_t y) { this.x = x; this.y = y; } + + /// <summary> + /// Constructs a new <see cref="Vector2"/> from an existing <see cref="Vector2"/>. + /// </summary> + /// <param name="v">The existing <see cref="Vector2"/>.</param> public Vector2(Vector2 v) { x = v.x; @@ -453,6 +723,12 @@ namespace Godot return x == other.x && y == other.y; } + /// <summary> + /// Returns true if this vector and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other vector to compare.</param> + /// <returns>Whether or not the vectors are approximately equal.</returns> public bool IsEqualApprox(Vector2 other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs index 7dc22d7918..8dd9ab2f0d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2i.cs @@ -16,15 +16,29 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector2i : IEquatable<Vector2i> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, Y } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public int x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public int y; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`.</value> public int this[int index] { get @@ -55,56 +69,102 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(int)"/> called on each component.</returns> public Vector2i Abs() { return new Vector2i(Mathf.Abs(x), Mathf.Abs(y)); } + /// <summary> + /// Returns this vector's angle with respect to the X axis, or (1, 0) vector, in radians. + /// + /// Equivalent to the result of <see cref="Mathf.Atan2(real_t, real_t)"/> when + /// called with the vector's `y` and `x` as parameters: `Mathf.Atan2(v.y, v.x)`. + /// </summary> + /// <returns>The angle of this vector, in radians.</returns> public real_t Angle() { return Mathf.Atan2(y, x); } + /// <summary> + /// Returns the angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector2i to) { return Mathf.Atan2(Cross(to), Dot(to)); } + /// <summary> + /// Returns the angle between the line connecting the two points and the X axis, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleToPoint(Vector2i to) { return Mathf.Atan2(y - to.y, x - to.x); } + /// <summary> + /// Returns the aspect ratio of this vector, the ratio of `x` to `y`. + /// </summary> + /// <returns>The `x` component divided by the `y` component.</returns> public real_t Aspect() { return x / (real_t)y; } - public Vector2i Bounce(Vector2i n) - { - return -Reflect(n); - } - + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product vector.</returns> public int Cross(Vector2i b) { return x * b.y - y * b.x; } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public int DistanceSquaredTo(Vector2i b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector2i b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public int Dot(Vector2i b) { return x * b.x + y * b.y; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { int x2 = x * x; @@ -113,6 +173,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public int LengthSquared() { int x2 = x * x; @@ -121,16 +187,31 @@ namespace Godot return x2 + y2; } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? Axis.Y : Axis.X; } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If both components are equal, this method returns <see cref="Axis.Y"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { - return x > y ? Axis.Y : Axis.X; + return x < y ? Axis.X : Axis.Y; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `mod`.</returns> public Vector2i PosMod(int mod) { Vector2i v = this; @@ -139,6 +220,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `modv`'s components.</returns> public Vector2i PosMod(Vector2i modv) { Vector2i v = this; @@ -147,11 +233,12 @@ namespace Godot return v; } - public Vector2i Reflect(Vector2i n) - { - return 2 * Dot(n) * n - this; - } - + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(int)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector2i Sign() { Vector2i v = this; @@ -160,9 +247,14 @@ namespace Godot return v; } - public Vector2i Tangent() + /// <summary> + /// Returns a vector rotated 90 degrees counter-clockwise + /// compared to the original, with the same length. + /// </summary> + /// <returns>The perpendicular vector.</returns> + public Vector2 Perpendicular() { - return new Vector2i(y, -x); + return new Vector2(y, -x); } // Constants @@ -174,25 +266,64 @@ namespace Godot private static readonly Vector2i _right = new Vector2i(1, 0); private static readonly Vector2i _left = new Vector2i(-1, 0); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, 0)`</value> public static Vector2i Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector2i(1, 1)`</value> public static Vector2i One { get { return _one; } } + /// <summary> + /// Up unit vector. Y is down in 2D, so this vector points -Y. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, -1)`</value> public static Vector2i Up { get { return _up; } } + /// <summary> + /// Down unit vector. Y is down in 2D, so this vector points +Y. + /// </summary> + /// <value>Equivalent to `new Vector2i(0, 1)`</value> public static Vector2i Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the direction of right. + /// </summary> + /// <value>Equivalent to `new Vector2i(1, 0)`</value> public static Vector2i Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the direction of left. + /// </summary> + /// <value>Equivalent to `new Vector2i(-1, 0)`</value> public static Vector2i Left { get { return _left; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector2i"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> public Vector2i(int x, int y) { this.x = x; this.y = y; } + + /// <summary> + /// Constructs a new <see cref="Vector2i"/> from an existing <see cref="Vector2i"/>. + /// </summary> + /// <param name="vi">The existing <see cref="Vector2i"/>.</param> public Vector2i(Vector2i vi) { this.x = vi.x; this.y = vi.y; } + + /// <summary> + /// Constructs a new <see cref="Vector2i"/> from an existing <see cref="Vector2"/> + /// by rounding the components via <see cref="Mathf.RoundToInt(real_t)"/>. + /// </summary> + /// <param name="v">The <see cref="Vector2"/> to convert.</param> public Vector2i(Vector2 v) { this.x = Mathf.RoundToInt(v.x); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs index b26e17ecba..d9b16a6afd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -21,6 +21,10 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector3 : IEquatable<Vector3> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, @@ -28,10 +32,23 @@ namespace Godot Z } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public real_t x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public real_t y; + /// <summary> + /// The vector's Z component. Also accessible by using the index position `[2]`. + /// </summary> public real_t z; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`.</value> public real_t this[int index] { get @@ -84,26 +101,49 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(real_t)"/> called on each component.</returns> public Vector3 Abs() { return new Vector3(Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z)); } + /// <summary> + /// Returns the minimum angle to the given vector, in radians. + /// </summary> + /// <param name="to">The other vector to compare this vector to.</param> + /// <returns>The angle between the two vectors, in radians.</returns> public real_t AngleTo(Vector3 to) { return Mathf.Atan2(Cross(to).Length(), Dot(to)); } - public Vector3 Bounce(Vector3 n) + /// <summary> + /// Returns this vector "bounced off" from a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to bounce off. Must be normalized.</param> + /// <returns>The bounced vector.</returns> + public Vector3 Bounce(Vector3 normal) { - return -Reflect(n); + return -Reflect(normal); } + /// <summary> + /// Returns a new vector with all components rounded up (towards positive infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Ceil"/> called on each component.</returns> public Vector3 Ceil() { return new Vector3(Mathf.Ceil(x), Mathf.Ceil(y), Mathf.Ceil(z)); } + /// <summary> + /// Returns the cross product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>The cross product vector.</returns> public Vector3 Cross(Vector3 b) { return new Vector3 @@ -114,12 +154,21 @@ namespace Godot ); } + /// <summary> + /// Performs a cubic interpolation between vectors `preA`, this vector, + /// `b`, and `postB`, by the given amount `t`. + /// </summary> + /// <param name="b">The destination vector.</param> + /// <param name="preA">A vector before this vector.</param> + /// <param name="postB">A vector after `b`.</param> + /// <param name="t">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The interpolated vector.</returns> public Vector3 CubicInterpolate(Vector3 b, Vector3 preA, Vector3 postB, real_t t) { - var p0 = preA; - var p1 = this; - var p2 = b; - var p3 = postB; + Vector3 p0 = preA; + Vector3 p1 = this; + Vector3 p2 = b; + Vector3 p3 = postB; real_t t2 = t * t; real_t t3 = t2 * t; @@ -131,41 +180,79 @@ namespace Godot ); } + /// <summary> + /// Returns the normalized vector pointing from this vector to `b`. + /// </summary> + /// <param name="b">The other vector to point towards.</param> + /// <returns>The direction from this vector to `b`.</returns> public Vector3 DirectionTo(Vector3 b) { return new Vector3(b.x - x, b.y - y, b.z - z).Normalized(); } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public real_t DistanceSquaredTo(Vector3 b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector3 b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public real_t Dot(Vector3 b) { return x * b.x + y * b.y + z * b.z; } + /// <summary> + /// Returns a new vector with all components rounded down (towards negative infinity). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Floor"/> called on each component.</returns> public Vector3 Floor() { return new Vector3(Mathf.Floor(x), Mathf.Floor(y), Mathf.Floor(z)); } + /// <summary> + /// Returns the inverse of this vector. This is the same as `new Vector3(1 / v.x, 1 / v.y, 1 / v.z)`. + /// </summary> + /// <returns>The inverse of this vector.</returns> public Vector3 Inverse() { - return new Vector3(1.0f / x, 1.0f / y, 1.0f / z); + return new Vector3(1 / x, 1 / y, 1 / z); } + /// <summary> + /// Returns true if the vector is normalized, and false otherwise. + /// </summary> + /// <returns>A bool indicating whether or not the vector is normalized.</returns> public bool IsNormalized() { return Mathf.Abs(LengthSquared() - 1.0f) < Mathf.Epsilon; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { real_t x2 = x * x; @@ -175,6 +262,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2 + z2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public real_t LengthSquared() { real_t x2 = x * x; @@ -184,6 +277,13 @@ namespace Godot return x2 + y2 + z2; } + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> public Vector3 Lerp(Vector3 to, real_t weight) { return new Vector3 @@ -194,6 +294,13 @@ namespace Godot ); } + /// <summary> + /// Returns the result of the linear interpolation between + /// this vector and `to` by the vector amount `weight`. + /// </summary> + /// <param name="to">The destination vector for interpolation.</param> + /// <param name="weight">A vector with components on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> public Vector3 Lerp(Vector3 to, Vector3 weight) { return new Vector3 @@ -204,24 +311,44 @@ namespace Godot ); } - public Vector3 MoveToward(Vector3 to, real_t delta) - { - var v = this; - var vd = to - v; - var len = vd.Length(); - return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; - } - + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? (y < z ? Axis.Z : Axis.Y) : (x < z ? Axis.Z : Axis.X); } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.Z"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { return x < y ? (x < z ? Axis.X : Axis.Z) : (y < z ? Axis.Y : Axis.Z); } + /// <summary> + /// Moves this vector toward `to` by the fixed `delta` amount. + /// </summary> + /// <param name="to">The vector to move towards.</param> + /// <param name="delta">The amount to move towards by.</param> + /// <returns>The resulting vector.</returns> + public Vector3 MoveToward(Vector3 to, real_t delta) + { + var v = this; + var vd = to - v; + var len = vd.Length(); + return len <= delta || len < Mathf.Epsilon ? to : v + vd / len * delta; + } + + /// <summary> + /// Returns the vector scaled to unit length. Equivalent to `v / v.Length()`. + /// </summary> + /// <returns>A normalized version of the vector.</returns> public Vector3 Normalized() { var v = this; @@ -229,6 +356,11 @@ namespace Godot return v; } + /// <summary> + /// Returns the outer product with `b`. + /// </summary> + /// <param name="b">The other vector.</param> + /// <returns>A <see cref="Basis"/> representing the outer product matrix.</returns> public Basis Outer(Vector3 b) { return new Basis( @@ -238,6 +370,11 @@ namespace Godot ); } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `mod`.</returns> public Vector3 PosMod(real_t mod) { Vector3 v; @@ -247,6 +384,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(real_t, real_t)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(real_t, real_t)"/> by `modv`'s components.</returns> public Vector3 PosMod(Vector3 modv) { Vector3 v; @@ -256,30 +398,66 @@ namespace Godot return v; } + /// <summary> + /// Returns this vector projected onto another vector `b`. + /// </summary> + /// <param name="onNormal">The vector to project onto.</param> + /// <returns>The projected vector.</returns> public Vector3 Project(Vector3 onNormal) { return onNormal * (Dot(onNormal) / onNormal.LengthSquared()); } - public Vector3 Reflect(Vector3 n) + /// <summary> + /// Returns this vector reflected from a plane defined by the given `normal`. + /// </summary> + /// <param name="normal">The normal vector defining the plane to reflect from. Must be normalized.</param> + /// <returns>The reflected vector.</returns> + public Vector3 Reflect(Vector3 normal) { #if DEBUG - if (!n.IsNormalized()) - throw new ArgumentException("Argument is not normalized", nameof(n)); + if (!normal.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(normal)); + } #endif - return 2.0f * n * Dot(n) - this; + return 2.0f * Dot(normal) * normal - this; } - public Vector3 Round() + /// <summary> + /// Rotates this vector around a given `axis` vector by `phi` radians. + /// The `axis` vector must be a normalized vector. + /// </summary> + /// <param name="axis">The vector to rotate around. Must be normalized.</param> + /// <param name="phi">The angle to rotate by, in radians.</param> + /// <returns>The rotated vector.</returns> + public Vector3 Rotated(Vector3 axis, real_t phi) { - return new Vector3(Mathf.Round(x), Mathf.Round(y), Mathf.Round(z)); +#if DEBUG + if (!axis.IsNormalized()) + { + throw new ArgumentException("Argument is not normalized", nameof(axis)); + } +#endif + return new Basis(axis, phi).Xform(this); } - public Vector3 Rotated(Vector3 axis, real_t phi) + /// <summary> + /// Returns this vector with all components rounded to the nearest integer, + /// with halfway cases rounded towards the nearest multiple of two. + /// </summary> + /// <returns>The rounded vector.</returns> + public Vector3 Round() { - return new Basis(axis, phi).Xform(this); + return new Vector3(Mathf.Round(x), Mathf.Round(y), Mathf.Round(z)); } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(real_t)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector3 Sign() { Vector3 v; @@ -289,44 +467,76 @@ namespace Godot return v; } - public Vector3 Slerp(Vector3 b, real_t t) + /// <summary> + /// Returns the result of the spherical linear interpolation between + /// this vector and `to` by amount `weight`. + /// + /// Note: Both vectors must be normalized. + /// </summary> + /// <param name="to">The destination vector for interpolation. Must be normalized.</param> + /// <param name="weight">A value on the range of 0.0 to 1.0, representing the amount of interpolation.</param> + /// <returns>The resulting vector of the interpolation.</returns> + public Vector3 Slerp(Vector3 to, real_t weight) { #if DEBUG if (!IsNormalized()) - throw new InvalidOperationException("Vector3 is not normalized"); + { + throw new InvalidOperationException("Vector3.Slerp: From vector is not normalized."); + } + if (!to.IsNormalized()) + { + throw new InvalidOperationException("Vector3.Slerp: `to` is not normalized."); + } #endif - real_t theta = AngleTo(b); - return Rotated(Cross(b), theta * t); + real_t theta = AngleTo(to); + return Rotated(Cross(to), theta * weight); } - public Vector3 Slide(Vector3 n) + /// <summary> + /// Returns this vector slid along a plane defined by the given normal. + /// </summary> + /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <returns>The slid vector.</returns> + public Vector3 Slide(Vector3 normal) { - return this - n * Dot(n); + return this - normal * Dot(normal); } - public Vector3 Snapped(Vector3 by) + /// <summary> + /// Returns this vector with each component snapped to the nearest multiple of `step`. + /// This can also be used to round to an arbitrary number of decimals. + /// </summary> + /// <param name="step">A vector value representing the step size to snap to.</param> + /// <returns>The snapped vector.</returns> + public Vector3 Snapped(Vector3 step) { return new Vector3 ( - Mathf.Stepify(x, by.x), - Mathf.Stepify(y, by.y), - Mathf.Stepify(z, by.z) + Mathf.Stepify(x, step.x), + Mathf.Stepify(y, step.y), + Mathf.Stepify(z, step.z) ); } + /// <summary> + /// Returns a diagonal matrix with the vector as main diagonal. + /// + /// This is equivalent to a Basis with no rotation or shearing and + /// this vector's components set as the scale. + /// </summary> + /// <returns>A Basis with the vector as its main diagonal.</returns> public Basis ToDiagonalMatrix() { return new Basis( - x, 0f, 0f, - 0f, y, 0f, - 0f, 0f, z + x, 0, 0, + 0, y, 0, + 0, 0, z ); } // Constants private static readonly Vector3 _zero = new Vector3(0, 0, 0); private static readonly Vector3 _one = new Vector3(1, 1, 1); - private static readonly Vector3 _negOne = new Vector3(-1, -1, -1); private static readonly Vector3 _inf = new Vector3(Mathf.Inf, Mathf.Inf, Mathf.Inf); private static readonly Vector3 _up = new Vector3(0, 1, 0); @@ -336,25 +546,74 @@ namespace Godot private static readonly Vector3 _forward = new Vector3(0, 0, -1); private static readonly Vector3 _back = new Vector3(0, 0, 1); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, 0)`</value> public static Vector3 Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector3(1, 1, 1)`</value> public static Vector3 One { get { return _one; } } - public static Vector3 NegOne { get { return _negOne; } } + /// <summary> + /// Infinity vector, a vector with all components set to `Mathf.Inf`. + /// </summary> + /// <value>Equivalent to `new Vector3(Mathf.Inf, Mathf.Inf, Mathf.Inf)`</value> public static Vector3 Inf { get { return _inf; } } + /// <summary> + /// Up unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 1, 0)`</value> public static Vector3 Up { get { return _up; } } + /// <summary> + /// Down unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3(0, -1, 0)`</value> public static Vector3 Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the local direction of right, + /// and the global direction of east. + /// </summary> + /// <value>Equivalent to `new Vector3(1, 0, 0)`</value> public static Vector3 Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the local direction of left, + /// and the global direction of west. + /// </summary> + /// <value>Equivalent to `new Vector3(-1, 0, 0)`</value> public static Vector3 Left { get { return _left; } } + /// <summary> + /// Forward unit vector. Represents the local direction of forward, + /// and the global direction of north. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, -1)`</value> public static Vector3 Forward { get { return _forward; } } + /// <summary> + /// Back unit vector. Represents the local direction of back, + /// and the global direction of south. + /// </summary> + /// <value>Equivalent to `new Vector3(0, 0, 1)`</value> public static Vector3 Back { get { return _back; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector3"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> + /// <param name="z">The vector's Z component.</param> public Vector3(real_t x, real_t y, real_t z) { this.x = x; this.y = y; this.z = z; } + + /// <summary> + /// Constructs a new <see cref="Vector3"/> from an existing <see cref="Vector3"/>. + /// </summary> + /// <param name="v">The existing <see cref="Vector3"/>.</param> public Vector3(Vector3 v) { x = v.x; @@ -515,6 +774,12 @@ namespace Godot return x == other.x && y == other.y && z == other.z; } + /// <summary> + /// Returns true if this vector and `other` are approximately equal, by running + /// <see cref="Mathf.IsEqualApprox(real_t, real_t)"/> on each component. + /// </summary> + /// <param name="other">The other vector to compare.</param> + /// <returns>Whether or not the vectors are approximately equal.</returns> public bool IsEqualApprox(Vector3 other) { return Mathf.IsEqualApprox(x, other.x) && Mathf.IsEqualApprox(y, other.y) && Mathf.IsEqualApprox(z, other.z); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs index c17f900131..bf25ba9cb3 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3i.cs @@ -16,6 +16,10 @@ namespace Godot [StructLayout(LayoutKind.Sequential)] public struct Vector3i : IEquatable<Vector3i> { + /// <summary> + /// Enumerated index values for the axes. + /// Returned by <see cref="MaxAxis"/> and <see cref="MinAxis"/>. + /// </summary> public enum Axis { X = 0, @@ -23,10 +27,23 @@ namespace Godot Z } + /// <summary> + /// The vector's X component. Also accessible by using the index position `[0]`. + /// </summary> public int x; + /// <summary> + /// The vector's Y component. Also accessible by using the index position `[1]`. + /// </summary> public int y; + /// <summary> + /// The vector's Z component. Also accessible by using the index position `[2]`. + /// </summary> public int z; + /// <summary> + /// Access vector components using their index. + /// </summary> + /// <value>`[0]` is equivalent to `.x`, `[1]` is equivalent to `.y`, `[2]` is equivalent to `.z`.</value> public int this[int index] { get @@ -62,39 +79,51 @@ namespace Godot } } + /// <summary> + /// Returns a new vector with all components in absolute values (i.e. positive). + /// </summary> + /// <returns>A vector with <see cref="Mathf.Abs(int)"/> called on each component.</returns> public Vector3i Abs() { - Vector3i v = this; - if (v.x < 0) - { - v.x = -v.x; - } - if (v.y < 0) - { - v.y = -v.y; - } - if (v.z < 0) - { - v.z = -v.z; - } - return v; + return new Vector3i(Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z)); } + /// <summary> + /// Returns the squared distance between this vector and `b`. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> public int DistanceSquaredTo(Vector3i b) { return (b - this).LengthSquared(); } + /// <summary> + /// Returns the distance between this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> public real_t DistanceTo(Vector3i b) { return (b - this).Length(); } + /// <summary> + /// Returns the dot product of this vector and `b`. + /// </summary> + /// <param name="b">The other vector to use.</param> + /// <returns>The dot product of the two vectors.</returns> public int Dot(Vector3i b) { return x * b.x + y * b.y + z * b.z; } + /// <summary> + /// Returns the length (magnitude) of this vector. + /// </summary> + /// <returns>The length of this vector.</returns> public real_t Length() { int x2 = x * x; @@ -104,6 +133,12 @@ namespace Godot return Mathf.Sqrt(x2 + y2 + z2); } + /// <summary> + /// Returns the squared length (squared magnitude) of this vector. + /// This method runs faster than <see cref="Length"/>, so prefer it if + /// you need to compare vectors or need the squared length for some formula. + /// </summary> + /// <returns>The squared length of this vector.</returns> public int LengthSquared() { int x2 = x * x; @@ -113,16 +148,31 @@ namespace Godot return x2 + y2 + z2; } + /// <summary> + /// Returns the axis of the vector's largest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.X"/>. + /// </summary> + /// <returns>The index of the largest axis.</returns> public Axis MaxAxis() { return x < y ? (y < z ? Axis.Z : Axis.Y) : (x < z ? Axis.Z : Axis.X); } + /// <summary> + /// Returns the axis of the vector's smallest value. See <see cref="Axis"/>. + /// If all components are equal, this method returns <see cref="Axis.Z"/>. + /// </summary> + /// <returns>The index of the smallest axis.</returns> public Axis MinAxis() { return x < y ? (x < z ? Axis.X : Axis.Z) : (y < z ? Axis.Y : Axis.Z); } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `mod`. + /// </summary> + /// <param name="mod">A value representing the divisor of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `mod`.</returns> public Vector3i PosMod(int mod) { Vector3i v = this; @@ -132,6 +182,11 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector composed of the <see cref="Mathf.PosMod(int, int)"/> of this vector's components and `modv`'s components. + /// </summary> + /// <param name="modv">A vector representing the divisors of the operation.</param> + /// <returns>A vector with each component <see cref="Mathf.PosMod(int, int)"/> by `modv`'s components.</returns> public Vector3i PosMod(Vector3i modv) { Vector3i v = this; @@ -141,6 +196,12 @@ namespace Godot return v; } + /// <summary> + /// Returns a vector with each component set to one or negative one, depending + /// on the signs of this vector's components, or zero if the component is zero, + /// by calling <see cref="Mathf.Sign(int)"/> on each component. + /// </summary> + /// <returns>A vector with all components as either `1`, `-1`, or `0`.</returns> public Vector3i Sign() { Vector3i v = this; @@ -161,29 +222,81 @@ namespace Godot private static readonly Vector3i _forward = new Vector3i(0, 0, -1); private static readonly Vector3i _back = new Vector3i(0, 0, 1); + /// <summary> + /// Zero vector, a vector with all components set to `0`. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, 0)`</value> public static Vector3i Zero { get { return _zero; } } + /// <summary> + /// One vector, a vector with all components set to `1`. + /// </summary> + /// <value>Equivalent to `new Vector3i(1, 1, 1)`</value> public static Vector3i One { get { return _one; } } + /// <summary> + /// Up unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 1, 0)`</value> public static Vector3i Up { get { return _up; } } + /// <summary> + /// Down unit vector. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, -1, 0)`</value> public static Vector3i Down { get { return _down; } } + /// <summary> + /// Right unit vector. Represents the local direction of right, + /// and the global direction of east. + /// </summary> + /// <value>Equivalent to `new Vector3i(1, 0, 0)`</value> public static Vector3i Right { get { return _right; } } + /// <summary> + /// Left unit vector. Represents the local direction of left, + /// and the global direction of west. + /// </summary> + /// <value>Equivalent to `new Vector3i(-1, 0, 0)`</value> public static Vector3i Left { get { return _left; } } + /// <summary> + /// Forward unit vector. Represents the local direction of forward, + /// and the global direction of north. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, -1)`</value> public static Vector3i Forward { get { return _forward; } } + /// <summary> + /// Back unit vector. Represents the local direction of back, + /// and the global direction of south. + /// </summary> + /// <value>Equivalent to `new Vector3i(0, 0, 1)`</value> public static Vector3i Back { get { return _back; } } - // Constructors + /// <summary> + /// Constructs a new <see cref="Vector3i"/> with the given components. + /// </summary> + /// <param name="x">The vector's X component.</param> + /// <param name="y">The vector's Y component.</param> + /// <param name="z">The vector's Z component.</param> public Vector3i(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } + + /// <summary> + /// Constructs a new <see cref="Vector3i"/> from an existing <see cref="Vector3i"/>. + /// </summary> + /// <param name="vi">The existing <see cref="Vector3i"/>.</param> public Vector3i(Vector3i vi) { this.x = vi.x; this.y = vi.y; this.z = vi.z; } + + /// <summary> + /// Constructs a new <see cref="Vector3i"/> from an existing <see cref="Vector3"/> + /// by rounding the components via <see cref="Mathf.RoundToInt(real_t)"/>. + /// </summary> + /// <param name="v">The <see cref="Vector3"/> to convert.</param> public Vector3i(Vector3 v) { this.x = Mathf.RoundToInt(v.x); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index 06ec2483c8..86a16c17f1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -1,39 +1,17 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{AEBF0036-DA76-4341-B651-A3F2856AB2FA}</ProjectGuid> - <OutputType>Library</OutputType> <OutputPath>bin/$(Configuration)</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <RootNamespace>Godot</RootNamespace> - <AssemblyName>GodotSharp</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <TargetFramework>netstandard2.1</TargetFramework> <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile> - <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath> + <EnableDefaultItems>false</EnableDefaultItems> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> + <PropertyGroup> + <DefineConstants>$(DefineConstants);GODOT</DefineConstants> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> <Compile Include="Core\AABB.cs" /> <Compile Include="Core\Array.cs" /> <Compile Include="Core\Attributes\ExportAttribute.cs" /> @@ -90,5 +68,4 @@ Fortunately code completion, go to definition and such still work. --> <Import Project="Generated\GeneratedIncludes.props" /> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> </Project> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs index f84e0183f6..da6f293871 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs @@ -1,27 +1,3 @@ -using System.Reflection; using System.Runtime.CompilerServices; -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharp")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] [assembly: InternalsVisibleTo("GodotSharpEditor")] diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 8785931312..a8c4ba96b5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -1,46 +1,26 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{8FBEC238-D944-4074-8548-B3B524305905}</ProjectGuid> - <OutputType>Library</OutputType> <OutputPath>bin/$(Configuration)</OutputPath> + <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <RootNamespace>Godot</RootNamespace> - <AssemblyName>GodotSharpEditor</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <TargetFramework>netstandard2.1</TargetFramework> <DocumentationFile>$(OutputPath)/$(AssemblyName).xml</DocumentationFile> - <BaseIntermediateOutputPath>obj</BaseIntermediateOutputPath> + <EnableDefaultItems>false</EnableDefaultItems> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>portable</DebugType> - <Optimize>false</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;DEBUG;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>portable</DebugType> - <Optimize>true</Optimize> - <DefineConstants>$(GodotDefineConstants);GODOT;</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <ConsolePause>false</ConsolePause> + <PropertyGroup> + <DefineConstants>$(DefineConstants);GODOT</DefineConstants> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> - <Import Project="Generated\GeneratedIncludes.props" /> - <ItemGroup> <ProjectReference Include="..\GodotSharp\GodotSharp.csproj"> - <Private>False</Private> + <Private>false</Private> </ProjectReference> </ItemGroup> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> + <!-- + We import a props file with auto-generated includes. This works well with Rider. + However, Visual Studio and MonoDevelop won't list them in the solution explorer. + We can't use wildcards as there may be undesired old files still hanging around. + Fortunately code completion, go to definition and such still work. + --> + <Import Project="Generated\GeneratedIncludes.props" /> </Project> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs deleted file mode 100644 index 3684b7a3cb..0000000000 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("GodotSharpEditor")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/modules/mono/mono_gd/gd_mono_assembly.cpp b/modules/mono/mono_gd/gd_mono_assembly.cpp index a170fd36e7..9dbeee57ce 100644 --- a/modules/mono/mono_gd/gd_mono_assembly.cpp +++ b/modules/mono/mono_gd/gd_mono_assembly.cpp @@ -425,7 +425,7 @@ GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class) while (!nested_classes.empty()) { GDMonoClass *current_nested = nested_classes.front()->get(); - nested_classes.pop_back(); + nested_classes.pop_front(); void *iter = nullptr; diff --git a/modules/mono/utils/osx_utils.cpp b/modules/mono/utils/osx_utils.cpp index 8e3e51e688..e68466b1cf 100644 --- a/modules/mono/utils/osx_utils.cpp +++ b/modules/mono/utils/osx_utils.cpp @@ -38,24 +38,21 @@ #include <CoreServices/CoreServices.h> bool osx_is_app_bundle_installed(const String &p_bundle_id) { - CFURLRef app_url = nullptr; CFStringRef bundle_id = CFStringCreateWithCString(nullptr, p_bundle_id.utf8(), kCFStringEncodingUTF8); - OSStatus result = LSFindApplicationForInfo(kLSUnknownCreator, bundle_id, nullptr, nullptr, &app_url); + CFArrayRef result = LSCopyApplicationURLsForBundleIdentifier(bundle_id, nullptr); CFRelease(bundle_id); - if (app_url) - CFRelease(app_url); - - switch (result) { - case noErr: + if (result) { + if (CFArrayGetCount(result) > 0) { + CFRelease(result); return true; - case kLSApplicationNotFoundErr: - break; - default: - break; + } else { + CFRelease(result); + return false; + } + } else { + return false; } - - return false; } #endif diff --git a/modules/regex/doc_classes/RegEx.xml b/modules/regex/doc_classes/RegEx.xml index c00fa96b2e..312275842a 100644 --- a/modules/regex/doc_classes/RegEx.xml +++ b/modules/regex/doc_classes/RegEx.xml @@ -11,7 +11,7 @@ regex.compile("\\w-(\\d+)") [/codeblock] The search pattern must be escaped first for GDScript before it is escaped for the expression. For example, [code]compile("\\d+")[/code] would be read by RegEx as [code]\d+[/code]. Similarly, [code]compile("\"(?:\\\\.|[^\"])*\"")[/code] would be read as [code]"(?:\\.|[^"])*"[/code]. - Using [method search] you can find the pattern within the given text. If a pattern is found, [RegExMatch] is returned and you can retrieve details of the results using functions such as [method RegExMatch.get_string] and [method RegExMatch.get_start]. + Using [method search], you can find the pattern within the given text. If a pattern is found, [RegExMatch] is returned and you can retrieve details of the results using methods such as [method RegExMatch.get_string] and [method RegExMatch.get_start]. [codeblock] var regex = RegEx.new() regex.compile("\\w-(\\d+)") @@ -19,7 +19,7 @@ if result: print(result.get_string()) # Would print n-0123 [/codeblock] - The results of capturing groups [code]()[/code] can be retrieved by passing the group number to the various functions in [RegExMatch]. Group 0 is the default and will always refer to the entire pattern. In the above example, calling [code]result.get_string(1)[/code] would give you [code]0123[/code]. + The results of capturing groups [code]()[/code] can be retrieved by passing the group number to the various methods in [RegExMatch]. Group 0 is the default and will always refer to the entire pattern. In the above example, calling [code]result.get_string(1)[/code] would give you [code]0123[/code]. This version of RegEx also supports named capturing groups, and the names can be used to retrieve the results. If two or more groups have the same name, the name would only refer to the first one with a match. [codeblock] var regex = RegEx.new() @@ -34,6 +34,15 @@ print(result.get_string("digit")) # Would print 01 03 0 3f 42 [/codeblock] + [b]Example of splitting a string using a RegEx:[/b] + [codeblock] + var regex = RegEx.new() + regex.compile("\\S+") # Negated whitespace character class. + var results = [] + for match in regex.search_all("One Two \n\tThree"): + results.push_back(match.get_string()) + # The `results` array now contains "One", "Two", "Three". + [/codeblock] [b]Note:[/b] Godot's regex implementation is based on the [url=https://www.pcre.org/]PCRE2[/url] library. You can view the full pattern reference [url=https://www.pcre.org/current/doc/html/pcre2pattern.html]here[/url]. [b]Tip:[/b] You can use [url=https://regexr.com/]Regexr[/url] to test regular expressions online. </description> diff --git a/modules/visual_script/visual_script.cpp b/modules/visual_script/visual_script.cpp index f387c0f288..1af4b46e22 100644 --- a/modules/visual_script/visual_script.cpp +++ b/modules/visual_script/visual_script.cpp @@ -36,8 +36,6 @@ #include "scene/main/node.h" #include "visual_script_nodes.h" -#include <stdint.h> - //used by editor, this is not really saved void VisualScriptNode::set_breakpoint(bool p_breakpoint) { breakpoint = p_breakpoint; @@ -1764,11 +1762,7 @@ Variant VisualScriptInstance::_call_internal(const StringName &p_method, void *p } next = node->sequence_outputs[output]; - if (next) { - VSDEBUG("GOT NEXT NODE - " + itos(next->get_id())); - } else { - VSDEBUG("GOT NEXT NODE - NULL"); - } + VSDEBUG("GOT NEXT NODE - " + (next ? itos(next->get_id()) : "NULL")); } if (flow_stack) { diff --git a/modules/visual_script/visual_script_editor.cpp b/modules/visual_script/visual_script_editor.cpp index b7ca3c882b..5581ea9318 100644 --- a/modules/visual_script/visual_script_editor.cpp +++ b/modules/visual_script/visual_script_editor.cpp @@ -2513,6 +2513,8 @@ RES VisualScriptEditor::get_edited_resource() const { } void VisualScriptEditor::set_edited_resource(const RES &p_res) { + ERR_FAIL_COND(script.is_valid()); + ERR_FAIL_COND(p_res.is_null()); script = p_res; signal_editor->script = script; signal_editor->undo_redo = undo_redo; @@ -2533,6 +2535,9 @@ void VisualScriptEditor::set_edited_resource(const RES &p_res) { _update_members(); } +void VisualScriptEditor::enable_editor() { +} + Vector<String> VisualScriptEditor::get_functions() { return Vector<String>(); } @@ -2546,6 +2551,9 @@ String VisualScriptEditor::get_name() { if (script->get_path().find("local://") == -1 && script->get_path().find("::") == -1) { name = script->get_path().get_file(); if (is_unsaved()) { + if (script->get_path().empty()) { + name = TTR("[unsaved]"); + } name += "(*)"; } } else if (script->get_name() != "") { @@ -2562,7 +2570,11 @@ Ref<Texture2D> VisualScriptEditor::get_theme_icon() { } bool VisualScriptEditor::is_unsaved() { - return script->is_edited() || script->are_subnodes_edited(); + bool unsaved = + script->is_edited() || + script->are_subnodes_edited() || + script->get_path().empty(); // In memory. + return unsaved; } Variant VisualScriptEditor::get_edit_state() { @@ -4660,10 +4672,10 @@ void VisualScriptEditor::_member_option(int p_option) { } } -void VisualScriptEditor::add_syntax_highlighter(SyntaxHighlighter *p_highlighter) { +void VisualScriptEditor::add_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) { } -void VisualScriptEditor::set_syntax_highlighter(SyntaxHighlighter *p_highlighter) { +void VisualScriptEditor::set_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) { } void VisualScriptEditor::_bind_methods() { @@ -4686,6 +4698,8 @@ void VisualScriptEditor::_bind_methods() { ClassDB::bind_method("_update_members", &VisualScriptEditor::_update_members); ClassDB::bind_method("_generic_search", &VisualScriptEditor::_generic_search); + + ClassDB::bind_method(D_METHOD("add_syntax_highlighter", "highlighter"), &VisualScriptEditor::add_syntax_highlighter); } VisualScriptEditor::VisualScriptEditor() { diff --git a/modules/visual_script/visual_script_editor.h b/modules/visual_script/visual_script_editor.h index 28418965b8..0c5665cee8 100644 --- a/modules/visual_script/visual_script_editor.h +++ b/modules/visual_script/visual_script_editor.h @@ -288,12 +288,13 @@ protected: static void _bind_methods(); public: - virtual void add_syntax_highlighter(SyntaxHighlighter *p_highlighter) override; - virtual void set_syntax_highlighter(SyntaxHighlighter *p_highlighter) override; + virtual void add_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) override; + virtual void set_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) override; virtual void apply_code() override; virtual RES get_edited_resource() const override; virtual void set_edited_resource(const RES &p_res) override; + virtual void enable_editor() override; virtual Vector<String> get_functions() override; virtual void reload_text() override; virtual String get_name() override; diff --git a/modules/visual_script/visual_script_expression.cpp b/modules/visual_script/visual_script_expression.cpp index bd41117497..2ac7793b8c 100644 --- a/modules/visual_script/visual_script_expression.cpp +++ b/modules/visual_script/visual_script_expression.cpp @@ -489,7 +489,7 @@ Error VisualScriptExpression::_get_token(Token &r_token) { r_token.type = TK_CONSTANT; if (is_float) { - r_token.value = num.to_double(); + r_token.value = num.to_float(); } else { r_token.value = num.to_int(); } |