Skip to content
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

Module piercing #37

Merged
merged 3 commits into from Dec 28, 2018
Merged

Module piercing #37

merged 3 commits into from Dec 28, 2018

Conversation

Protryon
Copy link
Contributor

This has a minor reducing impact on size for large module collections, however, this is mostly due to variable renaming to resolve conflicts. We will need DCE to get the benefits out of this. The old loader is still available, working, and testing.

@bakkot
Copy link
Member

bakkot commented Oct 25, 2018

An idea (which we may or may not find valuable enough to pursue):

Test262 has some coverage of module semantics. We might consider adding those tests to this project - i.e., for each test which contains an import or export, compile it with this project, tack on the necessary harness files, and then run it in a JS engine to ensure the asserts all hold.

Thoughts?

"}.call(this,this).result);\n";
String expected = "\"use strict\";\n" +
"var c=\"c\"+d;\n" +
"var d=\"d\";\n" +

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

"var d=\"d\";\n" +
"var c=\"c\"+d;\n" +
"var b=\"b\"+d;\n" +
"var result=\"a\"+b+c;\n";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var declarations in a module are not global. They are in scripts. As such, I believe this should all be wrapped up in an IIFE:

(function(){
  "use strict";
  var d = "d";
  var c = "c" + d;
  var b = "b" + d;
  var result = "a" + b + c;
})();

This also removes the need for the ThisUndefiningReducer, because this is undefined in IIFEs with strict bodies.

@Protryon
Copy link
Contributor Author

Protryon commented Nov 1, 2018

@bakkot The test262 module integration seems like a good idea, will implement.

@Protryon
Copy link
Contributor Author

Protryon commented Nov 2, 2018

This currently does pass all tests, however, this requires shapesecurity/shift-java#216 and shapesecurity/shift-java#214 to properly parse and early error all tests.

.gitmodules Show resolved Hide resolved
@bakkot
Copy link
Member

bakkot commented Nov 2, 2018

@Protryon the test262 tests would be a lot more valuable if we actually executed the resulting bundle, rather than just compiling it.

@Protryon
Copy link
Contributor Author

@bakkot Ready for review

String actualPierced = TestUtils.toString(TestUtils.bundlePierced(BundlerOptions.SPEC_OPTIONS, "/js1.js", resolver, localLoader));
String actualStandard = TestUtils.toString(TestUtils.bundleStandard(BundlerOptions.SPEC_OPTIONS, "/js1.js", resolver, localLoader));

String expectedPierced = "(function(t){\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to the nice short output a previous version of this PR had? I know it's not correct in edge cases, but sometimes that's an acceptable tradeoff given the overhead here.

The short output being something like

(function(){
  "use strict";
  var d = "d";
  var c = "c" + d;
  var b = "b" + d;
  var result = "a" + b + c;
  return { result: result };
})();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a dangerLevel options, an enum which can be SAFE (previous operation), BALANCED (pretty much what you posted for example, but asserts imports are read-only), and DANGEROUS (what we had before, and exactly what you posted for example, with the object being predeclared before returning).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Do you think it would be possible to statically identify some subset of programs where a theoretically dangerous level is in fact safe? For example, if there's no cycles, no import *, and no attempt to write to an imported binding, does that make it the BALANCED level actually 100% safe? If so, a good followup (not necessarily part of this PR) would be to automatically choose a more efficient strategy when it's safe to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no cycles/self-imports, it's safe to assume there are no TDZ issues, however BALANCED also currently removes the Symbol.toStringTag entry on the module full-namespace import. This would require no occurrences of import * as X (as opposed to import *), or require us to assert that X[Symbol.toStringTag] is never accessed, which can be done in some cases (say only StaticMemberExpressions were the only accesses of the namespace reference ).

DANGEROUS mode could be identified by enabling it and having a different read-detection response. For example, if there is a possible read, throw a bundle-time error rather than inserting a lambda with a TypeError throw statement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would require no occurrences of import * as X

Sorry, yeah, that's what I meant.

say only StaticMemberExpressions were the only accesses of the namespace reference

And exported module does not export any (non-arrow) functions which refer to this or eval, which can also give you a reference to the namespace object. Probably this analysis is more trouble than it's worth, but the basic "there are no namespace exports" one seems doable.

For example, if there is a possible read

Do you mean a possible write, rather than a possible read?


I guess any such analysis also needs the assumption that each exported binding is written to exactly once, right? (Or I guess, to be precise, that there are no writes to exported bindings inside of closures; multiple writes at the top level would be acceptable.) Otherwise the whole "live binding" complication comes into play. Still seems doable though.

Copy link
Contributor Author

@Protryon Protryon Dec 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I mean't possible write. The exported binding can be written as much as desired from the exporting class, however, the imported binding in that module or others is unwritable (as opposed to immutable, as any changes from the underlying module will be immediately reflected in the imported binding). EDIT: As such, the analysis is quite easy: look for an AssignmentTarget that references an import-specifier-defined binding. No more analysis is needed to detect a TypeError will be thrown when executed, assuming no dynamic scope overriding declarations via eval is going on.

At present, DANGEROUS mode just turns off this TypeError generation. The advantage to this is, more or less, nothing. If this is present in a script, the purpose of succeeding in compilation is only to throw a TypeError immediately, as would normally happen in such a circumstance. Moving this behavior to a compile time error is reasonable, for say, BALANCED mode, rather than ignoring it as in DANGEROUS mode.

@bakkot bakkot closed this Dec 4, 2018
@bakkot bakkot changed the base branch from master to es2017 December 5, 2018 00:04
@bakkot bakkot reopened this Dec 5, 2018
} else if (options.importUnresolvedResolutionStrategy == BundlerOptions.ImportUnresolvedResolutionStrategy.THROW_ON_REFERENCE) {
return new CallExpression(new FunctionExpression(false, false, Maybe.empty(), new FormalParameters(ImmutableList.empty(), Maybe.empty()),
new FunctionBody(ImmutableList.empty(), ImmutableList.of(new ThrowStatement(
new NewExpression(new StaticMemberExpression(new IdentifierExpression(globalBinding), "ReferenceError"), ImmutableList.empty())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you'll want to include an error string that refers to the unresolved binding?

public static @NotNull
Script bundle(@NotNull Path filePath) throws ModuleLoaderException {
return bundle(filePath, new FileSystemResolver(), new FileLoader(), new StandardModuleBundler());
public static @NotNull Script bundle(@NotNull BundlerOptions options, @NotNull Path filePath) throws ModuleLoaderException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep one that doesn't require BundlerOptions and just uses some reasonable default?

public final boolean throwOnImportAssignment;


private BundlerOptions(@Nonnull ImportUnresolvedResolutionStrategy importUnresolvedResolutionStrategy, @Nonnull ExportStrategy exportStrategy, @Nonnull DangerLevel dangerLevel, boolean throwOnCircularDependency, boolean throwOnImportAssignment) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make this public. It'd be annoying to have to start from one of NODE_OPTIONS or SPEC_OPTIONS.

@michaelficarra michaelficarra merged commit d59d5fe into shapesecurity:es2017 Dec 28, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants