diff --git a/modules/futures/.gitignore b/modules/futures/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/modules/futures/Futures.yyp b/modules/futures/Futures.yyp new file mode 100644 index 00000000..755ac9b2 --- /dev/null +++ b/modules/futures/Futures.yyp @@ -0,0 +1,36 @@ +{ + "resourceType": "GMProject", + "resourceVersion": "1.7", + "name": "Futures", + "AudioGroups": [ + {"resourceType":"GMAudioGroup","resourceVersion":"1.3","name":"audiogroup_default","targets":-1,}, + ], + "configs": { + "children": [], + "name": "Default", + }, + "defaultScriptType": 0, + "Folders": [ + {"resourceType":"GMFolder","resourceVersion":"1.0","name":"Rooms","folderPath":"folders/Rooms.yy",}, + {"resourceType":"GMFolder","resourceVersion":"1.0","name":"Scripts","folderPath":"folders/Scripts.yy",}, + {"resourceType":"GMFolder","resourceVersion":"1.0","name":"Tests","folderPath":"folders/Tests.yy",}, + ], + "IncludedFiles": [], + "isEcma": false, + "LibraryEmitters": [], + "MetaData": { + "IDEVersion": "2023.800.0.406", + }, + "resources": [ + {"id":{"name":"Room1","path":"rooms/Room1/Room1.yy",},}, + {"id":{"name":"DistributedCallbacks","path":"scripts/DistributedCallbacks/DistributedCallbacks.yy",},}, + {"id":{"name":"o_test","path":"objects/o_test/o_test.yy",},}, + ], + "RoomOrderNodes": [ + {"roomId":{"name":"Room1","path":"rooms/Room1/Room1.yy",},}, + ], + "templateType": "game", + "TextureGroups": [ + {"resourceType":"GMTextureGroup","resourceVersion":"1.3","name":"Default","autocrop":true,"border":2,"compressFormat":"bz2","directory":"","groupParent":null,"isScaled":true,"loadType":"default","mipsToGenerate":0,"targets":-1,}, + ], +} \ No newline at end of file diff --git a/modules/futures/objects/o_test/Create_0.gml b/modules/futures/objects/o_test/Create_0.gml new file mode 100644 index 00000000..c315a4a8 --- /dev/null +++ b/modules/futures/objects/o_test/Create_0.gml @@ -0,0 +1,40 @@ +show_debug_message("Distributed callback test"); + +step = 0; + +/// @type {Function.distributed_callback_signature} +var to_distribute = function (results){ + show_debug_message("distributed callback."+ + " Callback Idx: " + string(results.current_callback_idx) + + " Last value: " + string(results.last_value) + + " Step: " + string(o_test.step) + + " Done: " + string(results.done) + + " Aborted: " + string(results.aborted)); + if(results.current_callback_idx != o_test.step){ + throw("Not being distributed across steps!"); + } + if(o_test.step > 0 && results.last_value != o_test.step-1){ + throw("Values not being properly passed!"); + } + return o_test.step; +} + +distribute_over_steps([ + to_distribute, + to_distribute, + to_distribute, + to_distribute, + to_distribute, + to_distribute, + to_distribute, + to_distribute +], { + /// @param {Struct.DistributedCallbacks} results + on_done: function(results){ + show_debug_message("Distributed callback test done."); + if(!results.done){ + throw("Distributed callback test not done!"); + } + game_end(); + }, +}); diff --git a/modules/futures/objects/o_test/Step_0.gml b/modules/futures/objects/o_test/Step_0.gml new file mode 100644 index 00000000..e69de29b diff --git a/modules/futures/objects/o_test/Step_1.gml b/modules/futures/objects/o_test/Step_1.gml new file mode 100644 index 00000000..d859e11e --- /dev/null +++ b/modules/futures/objects/o_test/Step_1.gml @@ -0,0 +1 @@ +step ++; \ No newline at end of file diff --git a/modules/futures/objects/o_test/o_test.yy b/modules/futures/objects/o_test/o_test.yy new file mode 100644 index 00000000..efe9a135 --- /dev/null +++ b/modules/futures/objects/o_test/o_test.yy @@ -0,0 +1,34 @@ +{ + "resourceType": "GMObject", + "resourceVersion": "1.0", + "name": "o_test", + "eventList": [ + {"resourceType":"GMEvent","resourceVersion":"1.0","name":"","collisionObjectId":null,"eventNum":0,"eventType":0,"isDnD":false,}, + {"resourceType":"GMEvent","resourceVersion":"1.0","name":"","collisionObjectId":null,"eventNum":1,"eventType":3,"isDnD":false,}, + ], + "managed": true, + "overriddenProperties": [], + "parent": { + "name": "Tests", + "path": "folders/Tests.yy", + }, + "parentObjectId": null, + "persistent": false, + "physicsAngularDamping": 0, + "physicsDensity": 0, + "physicsFriction": 0, + "physicsGroup": 1, + "physicsKinematic": false, + "physicsLinearDamping": 0, + "physicsObject": false, + "physicsRestitution": 0, + "physicsSensor": false, + "physicsShape": 1, + "physicsShapePoints": [], + "physicsStartAwake": true, + "properties": [], + "solid": false, + "spriteId": null, + "spriteMaskId": null, + "visible": true, +} \ No newline at end of file diff --git a/modules/futures/rooms/Room1/Room1.yy b/modules/futures/rooms/Room1/Room1.yy new file mode 100644 index 00000000..25a76771 --- /dev/null +++ b/modules/futures/rooms/Room1/Room1.yy @@ -0,0 +1,55 @@ +{ + "resourceType": "GMRoom", + "resourceVersion": "1.0", + "name": "Room1", + "creationCodeFile": "", + "inheritCode": false, + "inheritCreationOrder": false, + "inheritLayers": false, + "instanceCreationOrder": [ + {"name":"inst_450597F8","path":"rooms/Room1/Room1.yy",}, + ], + "isDnd": false, + "layers": [ + {"resourceType":"GMRInstanceLayer","resourceVersion":"1.0","name":"Instances","depth":0,"effectEnabled":true,"effectType":null,"gridX":32,"gridY":32,"hierarchyFrozen":false,"inheritLayerDepth":false,"inheritLayerSettings":false,"inheritSubLayers":true,"inheritVisibility":true,"instances":[ + {"resourceType":"GMRInstance","resourceVersion":"1.0","name":"inst_450597F8","colour":4294967295,"frozen":false,"hasCreationCode":false,"ignore":false,"imageIndex":0,"imageSpeed":1.0,"inheritCode":false,"inheritedItemId":null,"inheritItemSettings":false,"isDnd":false,"objectId":{"name":"o_test","path":"objects/o_test/o_test.yy",},"properties":[],"rotation":0.0,"scaleX":1.0,"scaleY":1.0,"x":480.0,"y":288.0,}, + ],"layers":[],"properties":[],"userdefinedDepth":false,"visible":true,}, + {"resourceType":"GMRBackgroundLayer","resourceVersion":"1.0","name":"Background","animationFPS":15.0,"animationSpeedType":0,"colour":4278190080,"depth":100,"effectEnabled":true,"effectType":null,"gridX":32,"gridY":32,"hierarchyFrozen":false,"hspeed":0.0,"htiled":false,"inheritLayerDepth":false,"inheritLayerSettings":false,"inheritSubLayers":true,"inheritVisibility":true,"layers":[],"properties":[],"spriteId":null,"stretch":false,"userdefinedAnimFPS":false,"userdefinedDepth":false,"visible":true,"vspeed":0.0,"vtiled":false,"x":0,"y":0,}, + ], + "parent": { + "name": "Rooms", + "path": "folders/Rooms.yy", + }, + "parentRoom": null, + "physicsSettings": { + "inheritPhysicsSettings": false, + "PhysicsWorld": false, + "PhysicsWorldGravityX": 0.0, + "PhysicsWorldGravityY": 10.0, + "PhysicsWorldPixToMetres": 0.1, + }, + "roomSettings": { + "Height": 768, + "inheritRoomSettings": false, + "persistent": false, + "Width": 1366, + }, + "sequenceId": null, + "views": [ + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + {"hborder":32,"hport":768,"hspeed":-1,"hview":768,"inherit":false,"objectId":null,"vborder":32,"visible":false,"vspeed":-1,"wport":1366,"wview":1366,"xport":0,"xview":0,"yport":0,"yview":0,}, + ], + "viewSettings": { + "clearDisplayBuffer": true, + "clearViewBackground": false, + "enableViews": false, + "inheritViewSettings": false, + }, + "volume": 1.0, +} \ No newline at end of file diff --git a/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.gml b/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.gml new file mode 100644 index 00000000..d670184f --- /dev/null +++ b/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.gml @@ -0,0 +1,130 @@ +function DistributedCallbacks () constructor { + done = false; + aborted = false; + + current_callback_idx = -1; + /// @type {Array} + callbacks = []; + /// @type {Struct.DistributeOverStepsOptions|Undefined} + options = undefined; + + /// The values returned by all prior callbacks, in order + /// @type {Array} + values = []; + /// The value returned by the prior callback + /// @type {Any} + last_value = undefined; + + static __done = function(){ + if(self.done){ + return; + } + self.done = true; + if( is_struct(self.options) && + is_callable(self.options[$ "on_done"]) + ){ + self.options.on_done(self); + } + } + + static next = function(){ + var this = self; + + self.current_callback_idx++; + if (self.current_callback_idx >= array_length(self.callbacks)){ + return self.__done(); + } + var next_callback = self.callbacks[self.current_callback_idx]; + // Unset to prevent memory leaks + self.callbacks[self.current_callback_idx] = undefined; + + // If we're already aborted, we're done! + if(self.aborted){ + self.callbacks = []; // Unset to remove refs to functions + return; + } + + // If this callback doesn't exist, go ahead to the next one. + if(is_undefined(next_callback)){ + return self.next(); + } + + // Call the callback + try{ + // Add an entry to the values prior to calling, in case it fails (so we don't get wonky indexes) + array_push(self.values, undefined); + self.last_value = next_callback(this); + self.values[array_length(self.values)] = self.last_value; + } + catch(err){ + self.last_value = undefined; + if(is_struct(self.options) && self.options[$ "bail"]){ + self.abort(); + } + if(is_struct(self.options) && is_callable(self.options[$ "on_error"])){ + self.options.on_error(err); + } + else{ + throw err; + } + } + + var was_last = self.current_callback_idx >= array_length(self.callbacks) - 1; + + // If we haven't aborted, punt the next callback to the next step + if(was_last){ + self.__done(); + } + else if(!self.aborted){ + var _next = method(this, self.next); + call_later(1, time_source_units_frames, _next); + } + } + + static abort = function(){ + self.aborted = true; + self.callbacks = []; // Unset to remove function refs for GC + + } +} + +/// Signature for functions that can be distributed over steps. +/// +/// @param {Struct.DistributedCallbacks} results The cumulative results of all prior callbacks +function distributed_callback_signature(results) {} + +/// Distrubute a sequence of callbacks across steps. Callbacks +/// are executed one at a time, in order, with a single-step wait +/// in between. Callbacks are passed a `results` object, which +/// includes cumalative info about the process along with an `abort` +/// function to terminate the distribution. +/// +/// @param {Array} callbacks +/// @param {Struct.DistributeOverStepsOptions} [options] +/// @returns {Struct.DistributedCallbacks} +function distribute_over_steps(callbacks, options){ + var results = new DistributedCallbacks(); + results.callbacks = callbacks; + results.options = options; + results.next(); + return results; +} + +function DistributeOverStepsOptions () constructor { + /// An optional function to call when all callbacks have been called. + /// @type {Function.distributed_callback_signature|Undefined} + on_done = undefined; + + /// If true, the distribution will abort if any callback throws + /// an error. Else the distribution will continue to the + /// next callback on error, assuming the error was handled with on_error + /// @type {Boolean|Undefined} + bail = undefined; + + /// If provided, this function will be called with the error + /// thrown by any callback. If not provided, errors will be + /// thrown. If `bail` is true AND this function is provided, + /// the distribution will abort on error. + /// @type {Function|Undefined} + on_error = undefined; +} \ No newline at end of file diff --git a/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.yy b/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.yy new file mode 100644 index 00000000..c1c5917f --- /dev/null +++ b/modules/futures/scripts/DistributedCallbacks/DistributedCallbacks.yy @@ -0,0 +1,11 @@ +{ + "resourceType": "GMScript", + "resourceVersion": "1.0", + "name": "DistributedCallbacks", + "isCompatibility": false, + "isDnD": false, + "parent": { + "name": "Scripts", + "path": "folders/Scripts.yy", + }, +} \ No newline at end of file diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 9f5f7867..d601d579 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -3,10 +3,10 @@ import type { GmlVisitor } from '../gml-cst.js'; import type { JsdocSummary } from './jsdoc.js'; import { GmlLexer } from './lexer.js'; import type { GmlParseError } from './project.diagnostics.js'; -import { Reference, ReferenceableType } from './project.location.js'; -import { Signifier } from './signifiers.js'; +import type { Reference, ReferenceableType } from './project.location.js'; +import type { Signifier } from './signifiers.js'; import { c, categories, t, tokens } from './tokens.js'; -import { Type, TypeStore } from './types.js'; +import type { Type, TypeStore } from './types.js'; export interface GmlParsed { lexed: ILexingResult; @@ -708,6 +708,8 @@ export interface VisitorContext { isStatic?: boolean; /** While processing a function expression or struct literal, the signifier may come from an assignment operation. */ signifier?: Signifier; + /** While processing a function expression, we may have expected type information for the value */ + type?: TypeStore; /** While processing `method()` calls, we may find the self-context * of the function in the second argument. */ diff --git a/packages/parser/src/visitor.functionExpression.ts b/packages/parser/src/visitor.functionExpression.ts index a580636d..13081d1d 100644 --- a/packages/parser/src/visitor.functionExpression.ts +++ b/packages/parser/src/visitor.functionExpression.ts @@ -75,6 +75,7 @@ export function visitFunctionExpression( signifier?.describe(docs?.jsdoc.description); const functionType = signifier?.getTypeByKind('Function') || + getTypeOfKind(ctx.type, 'Function')?.derive() || new Type('Function').named(functionName); signifier?.setType(functionType); if (signifier && docs?.jsdoc.deprecated) { diff --git a/packages/parser/src/visitor.identifierAccessor.ts b/packages/parser/src/visitor.identifierAccessor.ts index 3a9a7a4c..04867ca0 100644 --- a/packages/parser/src/visitor.identifierAccessor.ts +++ b/packages/parser/src/visitor.identifierAccessor.ts @@ -322,6 +322,9 @@ function processFunctionArguments( functionCtx.self = methodSelf; } const expectedType = functionType?.getParameter(argIdx); + if (expectedType) { + functionCtx.type = expectedType.type; + } if (token.children.jsdoc) { visitor.jsdoc(token.children.jsdoc[0].children, functionCtx); } diff --git a/packages/parser/src/visitor.ts b/packages/parser/src/visitor.ts index e0c69bf9..c2418d29 100644 --- a/packages/parser/src/visitor.ts +++ b/packages/parser/src/visitor.ts @@ -554,7 +554,7 @@ export class GmlSignifierVisitor extends GmlVisitorBase { const structFromDocs = ctx.docs?.type[0]?.kind === 'Struct' ? (ctx.docs?.type[0] as StructType) - : undefined; + : getTypeOfKind(ctx.type, 'Struct')?.derive(); const struct = ctx.signifier?.getTypeByKind('Struct') || structFromDocs || @@ -620,7 +620,11 @@ export class GmlSignifierVisitor extends GmlVisitorBase { this, { name, range, container: struct }, parts.assignmentRightHandSide, - { docs, ctx, instance: true }, + { + docs, + ctx: { ...ctx, type: struct.getMember(name)?.type }, + instance: true, + }, ); } else { // Then we're in short-hand mode, where the RHS has the same diff --git a/packages/vscode/src/config.mts b/packages/vscode/src/config.mts index 1901b576..2e8516a0 100644 --- a/packages/vscode/src/config.mts +++ b/packages/vscode/src/config.mts @@ -1,10 +1,6 @@ import type { Channel } from '@bscotch/gamemaker-releases'; import vscode from 'vscode'; -interface SpriteSourcesConfig { - [projectPath: string]: string[]; -} - class StitchConfig { public context!: vscode.ExtensionContext;