Feature Namespaces - "packages" - for ASH

Veracity

Developer
Staff member

Introduction


ASH scripts can import other ASH scripts. As implemented, conceptually, the imported file is textually inserted at the import statement. This can cause name clashes: if multiple scripts define identically named types, variables, or functions, you end up with compile errors.

I've been considering how to add "namespaces" to ASH: a way for scripts to define types, variables, and functions which do not cause name clashes when the script is imported.

We have the infrastructure: every "scope" - a block of code within {} - has its own symbol table of types, variables, and functions.

For example, a function has a scope:

Code:
void myFunc()
{
   typdef type1 ...;
   record record1 ...;
   int var1 ...;
   void func1()
   {
       ...
   }

   ...code using the above...
}

None of the "local" symbols are visible or usable outside of the function.
I do this All The Time.

What if we could assign a name to a scope and reference the "local" symbols by referencing that name?

That's what we'll investigate here.

Script Scopes


Every script has an implicit "global" scope: all types, variables, and functions within it are part of an unnamed scope.
(There is also an implicit "static scope" intended primarily for variables - whose names are added to the global scope - but who are initialized only once.)
Imported files, as implemented, add their symbols to the global scope of the script that imported them.

The structure of an ASH script:

Code:
script "name";
notify "user";
since rNNNNN;

import "lib1.ash";
import "dir/lib2.ash";

....code that can use everything imported, since it has been added to the global scope...

Note that imported files have their own scope - which we save and associate with the file name.
Imported files can import other files - and if they import a previously imported file, they get the same scope.

Note also that you can import multiple scripts with a "main" function - and that is the only point of the "script" directive: you can call main@script(...) in the outer script.

What if we called a "namespace" a package, and could define one like this:

Code:
package package1
{
   typdef type1 ...;
   record type2 ...;
   int var1 ...;
   void func1()
   {
       ...
   }

   ...code using the above...
}

and any script that has access to package1 could use the local symbols via package1.type1, package1.rec1, package1.var1, package1.func1, etc.?

Lexically nested packages could be a thing, but have some issues I'll discuss later, but consider this:

Code:
package name;

Would associate a name to the script's global scope and importing that script would not add its symbols to its own global scope.
Voila! No name clashes.

Example Script


Let's consider how this might look for a complicated package of scripts.
Let's call it, for the sake of argument, "autoscend".
(I know literally nothing about how the actual script of that name is structured.)

Code:
scripts/autoscend.ash
scripts/autoscend/adventuring.ash
scripts/autoscend/combat.ash
scripts/autoscend/consumption.ash
scripts/autoscend/utils.ash

autoscend.ash:

Code:
package autoscend;
import <autoscend/adventuring.ash>
import <autoscend/combat.ash>
import <autoscend/consumption.ash>
import <autoscend/utils.ash>
void func1();

autoscend/adventuring.ash:

Code:
package autoscend.adventuring;
import <autoscend/utils.ash>
void func1();

autoscend/utils.ash:

Code:
package autoscend.utils;
void func1();

And so on.

autoscend.ash could call func1(), adventuring.func1() and utils.func1() with no conflict.
adventuring.ash could call func1() and utils.func1() with no conflict.

If you had some sort of "wrapper" script and wanted to import autoscend's utility package:

Code:
import <autoscend/utils.ash>

Since you are not in the "autoscend" package, you'd have to call autoscend.utils.func1()

And if, gawds forbid, you wanted to import all of autoscend, you could call autoscend.main(...), whether or not autoscend used a "program" directive.

Nested Packages


What if we actually implemented a "package" keyword and allowed a package to be nested within another script?

If you wanted a local package which had access to types, variables and functions of the enclosing script, it could be useful, although I wouldn't necessarily do that as an alternative to function-local types, variables, and functions.
But here we run into the fact that ASH is a single pass compiler, and where do you put the local package?
We could allow something like:

Code:
package local;
int local.func1();

Alternatively, soup up the ASH Parser to, somehow, not require forward declarations at all (like Java), possibly by pre-parsing types, variables, function signatures, and automatically updating them when the package is declared. That's a project of its own.

Your turn!


Thoughts?
 
Note also that you can import multiple scripts with a "main" function - and that is the only point of the "script" directive: you can call main@script(...) in the outer script.
A possibly terrible idea I had when starting to read this post: what if we generalized this such that if you import a script with a conflicting function func (or, generally, any symbol), it's only imported as func@script? And otherwise (for backwards compatibility purposes), all other functions are imported both directly and within the @script "namespace"?

Alternatively, soup up the ASH Parser to, somehow, not require forward declarations at all (like Java), possibly by pre-parsing types, variables, function signatures, and automatically updating them when the package is declared. That's a project of its own.
The usual way to not require forward declarations is to just use a two-pass approach, as you hint at.
 
This is an exciting proposal! Thank you for taking this in hand.

I have a couple of thoughts/questions (I started out with two, but it's expanded as I've typed.)

Exporting to global scope:

There are (many?) situations where one desires a centralized or utility function to be available, and would prefer it to be available with minimal keystrokes. For example, I have a dp() debug-print function that I like to import, and while sepos_toolbox.util.dp() is clear and explicit, it's also a lot of extra characters for something I'm dropping in in dozens of places in a script. Being able to do something like

Code:
package sepos_toolbox.utils;
global void dp(string... args);

and then later, using it

Code:
import <sepos_toolbox/utils.ash> with globals

// later, calling the function:
dp("foobar"); // syntactic sugar for sepos_toolbox.utils.dp("foobar");

might be very useful. (I assume "make functions first class objects" is beyond the scope of things here, but if not - either because it's already the case or because it's not as daunting a change as I assume - then that solves this use case; I don't mind assigning a long explicit name to a short name.)

Access to enclosing scope:

"Define this top-level variable, then reference it somewhere else" is a common script-configuration process. In a case like the autoscend situation you describe, would there be a way for something in autoscend.utils to reference something in autoscend scope? For instance, would this work?

(autoscend/autoscend.ash)

Code:
package autoscend;
import <autoscend/utils.ash>

int get_debug_level() {
 return(4);
}

// [...]
utils.do_the_debug("the monster debug");

(autoscend/utils.ash)

Code:
package autoscend.utils;

void do_the_debug(string arg) {
  // does this work?
  if (autoscend.get_debug_level() > 2) {
    print(arg);
  }
}

and if that does work, what happens if you import autoscend/utils.ash not from something that is in package autoscend?

Circular imports:

A possible solution to the above is to require an explicit import <autoscend.ash> in autoscend/utils.ash.

Obviously infinite recursion of imports is bad, and also the ideal is to have the functions in the imported autoscend namespace to have access to the same scope as the functions in the original autoscend namespace. (To be clear: I don't know how to solve that most correctly and appropriately; just that it's an important thing that comes up when using a language.)

Backwards compatibility:

In some imagined future, all scripts that anyone cares about will have been updated with package declarations and all scripts that refer to them will have been updated with explicit namespace use. In the meantime, what is the behavior for importing a non-packaged script? And is there a way to reverse that behavior? (That is, if the behavior is "bring everything into global scope", how do you tell it to bring it into a namespace? If the behavior is a generated namespace, how do you slam everything into global scope instead?)

Overriding defined packages:

Similar to the above question: if I have two scripts, both of which quite rudely declare themselves as package generic_utility, and I import them both, what happens?

Can I override the package name that I import them as? say, like this:

Code:
import <script1/generic_utility.ash> as "specific_utility"
import <script2/generic_utility.ash>

specific_utility.foobar(); // defined in script1/generic_utility.ash
generic_utility.foobar(); // defined in script2/generic_utility.ash

If I don't do that, do I get a namespace collision, or do they both try to put everything into the generic_utility namespace and only get an error if they both define void foobar()?
 
“Making functions first class objects” is a project I started a while ago, but abandoned. It was, in fact, daunting - since I included arrow functions.

Perhaps I will return to it, by and by.

I want to make ASH arrays backed by an ArrayList first, and add some List functions.

So many projects, so much time. :)

I’ll comment on the rest of your thoughts sometime after dinner. Or tomorrow.
 
  • Like
Reactions: ziz
Quick comment - I am out for the evening, but can't help it it. :)

I don't particularly want imported packages to proclaim what gets into global.
I want that power to be in charge of the importing script.

The Java way to do importing a single symbol would not be for the imported package to say "hey! this is a global symbol. Take it!" but something like (in proposed ASH syntax):

Code:
import <xxx.ash>;  // into package myfiles.utils
import myfiles.utils.dp;
...
dp("this monster debug");

Java lets you import specifc classes & enums (at least).
ASH could let you import types/variables/functions (including all parameter lists)
 
Back
Top