-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Import statement please read #85
Comments
Yeah, at first I was a little confuse about the usage of the word import. Since most of the time import is used for importing an module or something a like. I think it would be correct to use include for static and import for runtime |
I think having two different keywords will work fine. However I would want to see if it would be possible to detect that a "static import" could be done though, so we could only have the import keyword. For instance if an import is done at the top of a file, out of all scope blocks and only in the main scope, or the import statement is a literal then it could do the "static import" instead of the "runtime import" and avoid confusion. We could make a note in the documentation about this. |
I like your suggestion @brandon-ray so: |
That would be nice. Although it would need to be "heavy" documented. Because, people would get confuse. |
Yes something like that @marcobambini. Though you should still be able to import dynamically at runtime even with a string literal. I suppose there would need to be a check for this, I could see the logic going something like this:
This is assuming that we can only do static imports for .gravity files though, which seems to be the case. |
Why an source file would not be available at compile time? |
It would cover the case that someone is using a conditional import like in an if block or a try catch block. If we throw an error at compile time then these will not be able to work. |
If you guys don't mind, I would like to suggest another way. What about have import statement for compile time import and import function for runtime import. Like this:
I don't know why but it makes more sense for me. |
why not:
|
@sargun we are trying to not introduce another reserved keyword |
In Brunch JavaScript projects, there are npm packages and source files. You can use the same keyword to import both, what differs is how you call them. Example: import { Ajax } from 'ajax-lib' // this lib comes from NPM
import { MySuperClass } from './helpers/MySuperClass' // the "./" assumes it isn't a npm package Perhaps something similar could be used in Gravity: import "binary" // assumes an already compiled source
import "gravity-compiled-file.g" // same as above
import "gravity-source-file.gravity" // assumes non-compiled file This way you would let Gravity knows how to import those files based on their extensions ( |
@kazzkiq the problem is not the place where lib or file is located. The problem is when import file, at compile time or runtime. |
@ecanuto Exactly. But how do you define what should be imported at runtime or compile time? My proposal is to let Gravity choose how to handle an import (add it at compile time or runtime) based on its file extension. |
@kazzkiq Maybe I am wrong but you suggest a way to import two kinds of files, one already compiled (byte code) and other not compiled (source code). The problem here is not the compiled or non compiled file, the problem is that we want to compile file at compile time or at runtime. Think about this example:
How we will decide when import binary1 and binary2, same syntax but I want to have it imported at different times. The file binary1 must exists when source code is compile but binary2 must exists only while executing program. I think that import instruction and import function could be more reasonable and instruction could be allowed only at beginning of program while function could be used at any place. |
Well, if the preference is to avoid an additional keyword, could we just re-use the
Otherwise, I'd vote for the In my opinion, using file paths or extensions to determine where to import modules from is a somewhat dangerous methodology, as you're removing choice and use-cases that might actually be beneficial for some people. |
Based on this discussion I'd vote for include for static and import for runtime. |
In order to not include a new keyword I think I am going to introduce a new macro (the other one is #unittest): Runtime import keyword will be a syntactic sugar for System.import so we can implement it without modifying the virtual machine. We need to find out which options and which syntax the runtime import keyword will support. |
Might be worth checking out Lua's require system. |
Although, that might imply a semantic change within the language, as lua can execute the file with the same environment, vs if it was copied verbatim. |
Maybe do it via |
Hey guys. :) I had previously opened an issue and was referred here, so I thought I would share my personal opinion as well. First: I think the import Audio from "audio"; But then, we had a new syntax to specify explicit pieces we wanted: import {Playback} from "audio"; So what I would NOT like Gravity to do, is to introduce default exports at all. Instead: import Foo, Bar from "module"; This would make people more aware of what they are to export. Thus, although it may sound a little bit like more work, picking exactly which things you are going to import from a model decreases the likelyhood of polluting a namespace. Plus, you actually are optimizing, as you only use what you really need, instead of importing pretty much everything - only to then figure out what you actually imported alltogether... Second: Using Third: NPM isn't exactly decentralized - where as for Gravity, I would totally prefer that. This way, people can easily host their own packages at any place. Fourth: Paths. I really like the idea of setting a library path and not hardcoding it - however, it might be easier to actually soft-code a path. Let's say, the user is trying out Gravity and just wants to fool around with it (which is what I usually do when I run across something cool...), he may not want to dig up environment variables or command line switches, and "just" wants to get going. Now, it could be possible to soft-code something like Now, this is kind of related here: File extensions. I like
My suggestion: Don't. Do. This. Fifth: FFI. Now, it really depends on an API if implementing native bindings through C is preferred over FFI or vice versa. But what if we just did not include this into the "core" - but instead made it an example module? What I am talking about is: https://github.com/libffi/libffi By stating that the preferred way is to use the C API, but allowing someone to create bindings through a port of libffi as well, we would be able to cherry-pick the desired method. Now, libffi only works for C - as far as I know - but it would still be very useful to have an alternative approach sometimes, if desired. To sum up: Prefer Gravity C-API based native modules over FFI, but make FFI available as an alternative AND as an example of a native module (well, complex example, I guess - but an example still!) Sixth: Defining modules natively. There are two ways in which a module may be loaded: Through the script, or through the C API. Now, what NodeJS does is this:
First, there are actually two types of modules: Internal ones, and externally loaded ones. Internal ones are basically caught in a struct (I didn't find that exact one, must've overseen it). This struct is basically being iterated at the beginning to "mass register" the modules to the whole system, allowing Node a much faster lookup - because those modules are already loaded, and do not need to be searched on disk first only to fall back to looking internally. So, being able to define modules that are "internal" - so, ones that are not actually available on disk, but rather the very library or binary - would probably be quite crucial. As for external modules, they simply load the very binary, craft a pointer to the given registration and unregistration function - basically, constructor and deconstructor for the lifetime of the binary. This constructor is usually passed some default values - which in this case, might be VM, or something like that - you get the idea. So should Gravity pick up a native module from the disk, it also needs to maintain it's lifetime throughout the process - and let them destruct at process end. But, that isn't all yet - nope! A similiar mechanic needs to also be applied to internal modules. The reason for that is quite simple: Let me take my concept of a build tool based on a complete scripting language for example here. In order to implement HTTP/S downloading into the system, I need to add a proper library-binding module right into the binary. Now, some of those actually have some initialization and de-initialization functions that need to be called - even when linked into the binary. So we would need to be able to also handle that scenario for internal modules too. However, this makes something really simple:
I mean, not everything is written in C when using an embedded scripting language. So being able to embed some pre-written scripts as an internal module, may come in quite handy. In my case, I am zipping up a Those are all pretty much my own opinion. I did read a lot of cool ideas above, but I am not very good in backtracking through a discussion and picking user names up in order to mention them - kind of a flaw related to my visual impairment which implies a miniature field of vision ^^; So, excuse me for that! I really hope to see this becoming a thing in the future! Also, sorry for the wall of text... I just realized how long this thing got... o.o; EDIT (1.40 AM, German time) I believe it should be a separate class. The reason for that is that you would be able to mirror the module cache, search paths and everything in one object/class rather than squeezing it into
By splitting the module loading into functions, embedders/users can decide which part they'd like to change. Here is a proposal for such a class: class Module {
// Encapsulate the cache - make it freely accessible internally,
// but read-only to the public.
// There is probably a more elegant way for this, though...
private static var _cache;
public static var cache {
get { return _cache; }
}
/**
* Store the search path for modules. Has to be used by .resolve().
* @type {Array}
*/
private static var _paths = [];
public static var paths {
get { return _paths; }
set {
if(value is Array) {
_paths = value;
} else {
// Actually, what is the proper way of throwing/causing an error?
triggerAnError("Can not set Module.paths to something else but Array.");
}
}
}
private static var _exts = NULL;
public static var extensions {
get {
if(this._exts == NULL) {
this._exts = new this.ModuleExtensions();
}
return this._exts;
}
}
private static class ModuleExtensions {
// Store a map of extensions and possibly additional processors.
// By default, this might be:
// ".g": Module.runScriptModule(...)
// ".n": Module.runBytecodeModule(...)
// ".gso": Module.runBinaryModule(...)
var extensions = {};
func addExt(ext, processor) {
if(!(ext in this.extensions)) {
causeAnError(
"Given extension is already set. Remove it first to"
+ " add a new processor for it. Overriding a processor is unsafe."
);
}
if(processor is Function) {
this.extensions[ext] = processor;
} else {
causeAnError(
"Given processor is of type \(typeof processor) - but Function was"
+ " expected."
);
}
}
func removeExt(ext) {
if(ext in this.extensions) {
delete this.extensions[ext];
return true;
} else {
return false;
}
}
func hasExt(ext) {
return (ext in this.extensions);
}
func getProcessor(ext) {
if(this.hasExt(ext))
return this.extensions[ext]
else
return false;
}
// Operator mapping. Im just gonna list instead of "implement" them:
// o[key]: Get processor
// o[key] = value: Set processor
// Call .removeExt() to remove. There might be a better way, though.
// Maybe when o[key] = value is used and value==NULL, we'll attempt to
// unset the processor?
}
/**
* Returns the exports required by `importsWanted` for a given module.
* @param moduleName: String containing the module name given in the
* import statement.
* @param importsWanted: Array of variables, objects, classes or alike, that
* the requesting module would like to import from the
* given `moduleName`.
*
* @return Should return the imports. Now, since the `import` statement by
* itself is actually syntactic shugar for this, there probably has
* to be some logic to disassemble the returned map and play out the
* new variables in the requesting scope.
*/
public static func load(moduleName, importsWanted) {
// ...
return imports;
}
/**
* Resolve `moduleName` to an actual file. In some scenarios, this one might
* actually be implemented in C rather than the script - but by default, is
* probably best implemented in the script - as C-Calls can stack up really
* fast - and something like resolving happens to look cleaner in a script
* than in C - at least, most of the time.
*
* @param moduleName: The name of the module that is to be resolved.
*
* @return A string to the actual file. We do NOT return the source here -
* because the exec*() methods may use custom processing. I.e., the
* script-oriented executor will use the raw text, whilst the
* Bytecode executor will operate slightly different. Therefore,
* this method shall only return a string, which should be used
* within the cache.
*/
public static func resolve(moduleName) {
// ...
return modulePath;
}
/**
* Those are supposed to be the module executors. Each of these is actually
* likely implemented in C rather than the script, as VM access is required.
* I still put them here for completeness.
*
* To all three of them applies:
* @param modulePath: A fully resolved path to the module that is to be
* imported. It should be returned by .resolve() and be
* requested by .load().
*
* Basically:
* 1. .load() receives the module name,
* 2. resolves it using .resolve()
* 3. executes it (either depending on file suffix or other method)
* 4. and finally, .load() returns the requested variables as a map.
*/
public static func runScriptModule(modulePath) { /* ... */ }
public static func runBinaryModule(modulePath) { /* ... */ }
public static func runBytecodeModule(modulePath) { /* ... */}
} This would also allow a JSON-processor, so we could import specific keys from JSON. import name, version from "./package.json"; And, we would get to redefine functions too - if the embedder or user really wants to mess this much with the module system. Appending to the module path would also be possible at any time. |
I just saw @Jezza's comment about Lua's require system - and although I think about NodeJS' A module system can be implemented without changing the syntax of the language at all. Imagine the following call: var audio = require("audio"); What That would be very much a non-syntax changing API, but only requires for the possibility of extracting values off one Fiber and transferring it over to another. func getExports(fullFilePath: string) {
var f = fiber.createFromFile(fullFilePath);
var fibReturn = f.runAndWait();
return fibReturn.exports;
}
// Within a file given to that function:
exports["foo"] = "bar";
// within a caller:
var myModule = require("./other.g");
System.print(myModule["foo"]); // "bar" However, this would quite obviously not be a very user-friendly syntax, since dot-notation for this would be nicer, which a map does not offer. :) But still, this is very possible. |
I personally really like how rust handles modules. It's a static language, so I'm not sure how well this will be received, but: You basically build a module tree. The entire tree will be available in the global namespace. For example, if you have a module While this doesn't allow dynamic loading of modules, which might still be wanted, it does allow modules to always be accessible. Circular imports are also not a thing, every module can use every other module. (Well, rust also has visibility, let's just assume everything is public) Especially for bigger and closely coupled projects, avoiding circular imports becomes a bit of a chore. Not having it be an issue at all is very nice. |
Hey hey :) Been a while since I last checked in with Gravity - and after checking the docs, I noticed that not a whole lot seems to have changed. So, I decided to check the issues and saw this open. What is the current state on the import statement - and, module loading in general? Thanks in advance! :) |
Hey! It's been a bit since this had some activity in it, so I'm currently working on this as a proof-of-concept. Personally, I like what @marcobambini proposed, using In testmodule.gravity class Test {
static func sayHello() {
System.print("Hello from the module!");
}
}
func export() {
return Test;
} In main.gravity var Test = System.import('testmodule');
func main() {
System.print("Hello from the main file!");
Test.sayHello();
} I've actually implemented this, already. You can check it out on my fork of this repo. Natives work too, but only on linux. The problem I'm having right now is not polluting the global namespace with unwanted junk from the library itself, but it's a concept right now anyways. I think Speaking of native libraries: In my opinion, libffi isn't needed (but could be implemented with a native library as a bridge). If people (read: Marco) aren't interested in importing anymore, I'd make it my own project. Edit: In my test you can't use |
Hello there @SarahIsWeird ! I have been checking Gravity's commit log on and off, hoping to spot the introduction of the I would love to use Gravity in my projects! So, yes, I am still very interested in the statement and features alltogether :) For a project I am working on I was considering to use JerryScript, but it is quite big and might actually be more overhead than what the project needs in terms of scripting. Gravity would work well there. I am going to check out your fork and see how it performs and how you implemented it in detail! Hopefuly, "native" Also thanks for the ping; I might've otherwise missed this post. ^^' Kind regards, |
Current import statement is a static operation performed at compile time, there is no runtime import statement in current version.
When you write:
import "file1.gravity"
what happens under the hood is that the content of file1.gravity is textually inserted in that position then parsed and compiled (it is far more efficient than this but you get the point).
It turns out that importing should be also a runtime operation in order to be able to import already compiled bytecode or shared libraries (and even source code I think).
So my first question is how do you think this ambiguity should be solved?
import at static time and import as runtime should have two different keyword?
For example include for static and import for runtime?
More question will follow up.
The text was updated successfully, but these errors were encountered: