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

Binding resolution refactored #5802

Merged
merged 9 commits into from
May 15, 2023
Merged

Binding resolution refactored #5802

merged 9 commits into from
May 15, 2023

Conversation

kraenhansen
Copy link
Member

@kraenhansen kraenhansen commented May 9, 2023

What, How & Why?

This fixes part of #5576 by declaring the output of the binding generator as a virtual "realm/binding" module, which we provide a path for implementation via tsconfig paths: A default fallback for tests (in packages/realm/tsconfig.json) as well as each of the platform specific directories.

It also removes the typescript-checker, as we concluded this might just be deferred and will happen as as part of building the SDK anyway.

@kraenhansen kraenhansen self-assigned this May 9, 2023
@cla-bot cla-bot bot added the cla: yes label May 9, 2023
@kraenhansen kraenhansen requested a review from RedBeard0531 May 9, 2023 12:29
coreOut("};");
}
coreOut(`
out("declare module 'realm/binding' {");
Copy link
Member Author

@kraenhansen kraenhansen May 9, 2023

Choose a reason for hiding this comment

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

This is one of the more important changes allowing us to import from "realm/binding" via paths in the tsconfig and have the types resolve as expected.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you are missing a word after "without"

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks - I updated the comment 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't love that this file is picked up by the src/*.ts glob and injects its contents into a virtual module rather than being found via an import path. But I don't currently have a great idea for an alternative if we want to move away from rollup's replace plugin.

Comment on lines 134 to 143
both("// Enums");
for (const e of spec.enums) {
// This pattern is copied from the TS compiler
both(`export var ${e.jsName};`);
both(`(function (${e.jsName}) {`);
both(
...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`),
);
both(`})(${e.jsName} || (${e.jsName} = {}));`);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Probably the most controversial change, but not a large price to pay to avoid relying on code replacements from the Rollup config.

Copy link
Contributor

Choose a reason for hiding this comment

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

const enums don't generate any js code: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM72MgrgWzgMQgjgG8BYAKDmrgDMiAaOAIwEMom2AvSgXyA. It only adds declarations to the .d.ts file.

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe const enum is only supposed to be used in non-declaration files.

Here's an example:

{
  "name": "ts-const-enums",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "tsx index.ts"
  },
  "dependencies": {
    "tsx": "^3.12.7",
    "typescript": "^5.0.4"
  }
}
// index.ts
import { Foo } from "./enums";
console.log(Foo.bar);
// enums.d.ts
export const enum Foo {
  bar = "bar"
}
// enums.js
// note how we provide no implementation

Running npm test yeilds:

/Users/kraen.hansen/Projects/ts-const-enums/index.ts:1
import { Foo } from "./enums";
         ^

SyntaxError: The requested module './enums' does not provide an export named 'Foo'
    at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:189:5)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:527:24)
    at async loadESM (node:internal/process/esm_loader:91:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12)

Even if we do provide a dummy implementation of Foo in enums.js:

export const Foo = {};

This simply prints undefined.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it behaves differently for string vs number const enums? When I tested locally with this patch, everything seems to work:

diff --git a/packages/realm/bindgen/src/templates/node-wrapper.ts b/packages/realm/bindgen/src/templates/node-wrapper.ts
index c66cf638c..364d7d840 100644
--- a/packages/realm/bindgen/src/templates/node-wrapper.ts
+++ b/packages/realm/bindgen/src/templates/node-wrapper.ts
@@ -131,17 +131,6 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {
     }
   `);

-  both("// Enums");
-  for (const e of spec.enums) {
-    // This pattern is copied from the TS compiler
-    both(`export var ${e.jsName};`);
-    both(`(function (${e.jsName}) {`);
-    both(
-      ...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`),
-    );
-    both(`})(${e.jsName} || (${e.jsName} = {}));`);
-  }
-
   const injectables = [
     "Long",
     "ArrayBuffer",
diff --git a/packages/realm/bindgen/src/templates/typescript.ts b/packages/realm/bindgen/src/templates/typescript.ts
index 3210f1f5c..0091d5f34 100644
--- a/packages/realm/bindgen/src/templates/typescript.ts
+++ b/packages/realm/bindgen/src/templates/typescript.ts
@@ -203,7 +203,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {
   out("// Enums");
   for (const e of spec.enums) {
     // Using const enum to avoid having to emit JS backing these
-    out(`export enum ${e.jsName} {`);
+    out(`export const enum ${e.jsName} {`);
     out(...e.enumerators.map(({ jsName, value }) => `${jsName} = ${value},\n`));
     out("};");
   }

Of course, I've often wondered whether we should be using enum or const enum for these. const enum may provide some perf and size benefits, but I don't know how much that matters given the JIT in node and the bytecode compiler for hermes. But it certainly shouldn't be slower, and I don't think we've needed the features of a non-const enum yet (eg mapping numeric values to/from string names at runtime)

Copy link
Member Author

Choose a reason for hiding this comment

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

When I apply this, the SDK unit test fails with:

✖ ERROR: TypeError: Cannot read properties of undefined (reading 'Get')
    at <anonymous> (/Users/kraen.hansen/Projects/realm-js/packages/realm/src/platform/network.ts:35:23)
    at Object.<anonymous> (/Users/kraen.hansen/Projects/realm-js/packages/realm/src/platform/network.ts:62:17)

@RedBeard0531
Copy link
Contributor

Do you mind holding off on any changes to bindgen until I get #5769 over the line. I hope to have it merged today or tomorrow.

@kraenhansen kraenhansen force-pushed the kh/binding-resolution branch from 5f08a18 to a7ee197 Compare May 10, 2023 08:56
Comment on lines 205 to 206
// Using const enum to avoid having to emit JS backing these
out(`export enum ${e.jsName} {`);
Copy link
Contributor

Choose a reason for hiding this comment

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

You lost the const in const enum

Copy link
Member Author

Choose a reason for hiding this comment

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

This is on purpose. Having a const enum in a non .d.ts file made the tests fail as it didn't have values at runtime. I suppose TSC only inlines these if they're in a TS source file.

Comment on lines 134 to 143
both("// Enums");
for (const e of spec.enums) {
// This pattern is copied from the TS compiler
both(`export var ${e.jsName};`);
both(`(function (${e.jsName}) {`);
both(
...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`),
);
both(`})(${e.jsName} || (${e.jsName} = {}));`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

const enums don't generate any js code: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM72MgrgWzgMQgjgG8BYAKDmrgDMiAaOAIwEMom2AvSgXyA. It only adds declarations to the .d.ts file.

Copy link
Contributor

Choose a reason for hiding this comment

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

Delete line 132 if you remove core.ts.

@kraenhansen kraenhansen force-pushed the kh/binding-resolution branch from a7ee197 to 3ffb6d6 Compare May 11, 2023 08:57
@kraenhansen kraenhansen requested a review from RedBeard0531 May 11, 2023 09:25
@kraenhansen
Copy link
Member Author

kraenhansen commented May 11, 2023

I've pushed a commit pointing core to realm/realm-core#6604, which I'll change once the Core PR has been merged and released.

@kraenhansen kraenhansen force-pushed the kh/binding-resolution branch from bd6a8cf to c42166a Compare May 11, 2023 10:21
Copy link
Contributor

@RedBeard0531 RedBeard0531 left a comment

Choose a reason for hiding this comment

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

Comments are just suggestions. Everything looks correct.

Comment on lines +29 to +31
function both(...content: string[]) {
reactLines.push(...content);
nodeLines.push(...content);
Copy link
Contributor

Choose a reason for hiding this comment

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

This change now seems unnecessary. But also probably harmless. 🤷

@@ -20,14 +20,15 @@ import { bindModel, Property } from "@realm/bindgen/bound-model";
import { TemplateContext } from "@realm/bindgen/context";

import { doJsPasses } from "../js-passes";
import { eslintFormatter } from "../formatters/eslint-formatter";
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure it is worth introducing a new directory for just this one file. Personally, since the formatters are tiny, I would just put all of them in a single ../formaters.ts file, even if we had more than one.

But at the very least, lets remove some redundancy in src/formatters/eslint-formatter.ts. If you want to keep one file per formatter, I'd go either src/formatters/eslint.ts or just src/eslint-formatter.ts. Slight preference for the latter if you don't want just src/formatters.ts

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved it one level up again 👍

@@ -31,6 +31,7 @@ import {
Pointer,
Template,
} from "@realm/bindgen/bound-model";
import { clangFormatter } from "@realm/bindgen/formatter";
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels a bit weird to call this clangFormatter since the tool is named clang-format, and the tool called clang doesn't do any formatting. I admit that clangFormatFormatter sounds ridiculous though. Maybe we should just call the formatters by the name of the tool with no suffix, so just clangFormat and eslint?

@@ -162,7 +149,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {

const spec = doJsPasses(bindModel(rawSpec));

const coreOut = file("core.ts", "eslint", "typescript-checker");
const coreOut = file("core.ts", eslintFormatter);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const coreOut = file("core.ts", eslintFormatter);
// This file is imported by the virtual path "realm/binding/core" in TS.
const coreOut = file("core.ts", eslintFormatter);

coreOut("};");
}
coreOut(`
out("declare module 'realm/binding' {");
Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't love that this file is picked up by the src/*.ts glob and injects its contents into a virtual module rather than being found via an import path. But I don't currently have a great idea for an alternative if we want to move away from rollup's replace plugin.

@@ -216,7 +205,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {

out("// Opaque types (including Key types)");
for (const { jsName } of (spec.opaqueTypes as NamedType[]).concat(spec.keyTypes)) {
out.lines("/** Using an empty enum to express a nominal type */", `export declare enum ${jsName} {}`);
out.lines("/** Using an empty enum to express a nominal type */", `export enum ${jsName} {}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
out.lines("/** Using an empty enum to express a nominal type */", `export enum ${jsName} {}`);
out.lines("/** Using an empty enum to express a nominal type */", `export const enum ${jsName} {}`);

We should probably consider some of the alternative mechanisms we discovered for opaque types at some point. But at the very least, we shouldn't imply that there is any actual value with that name.

Copy link
Member Author

@kraenhansen kraenhansen May 11, 2023

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

You mean like we already do for classes? Yes, possibly something like that. I wish there was a way to tell TS to model the behavior of Object.create(null) correctly, so that we could create a class that really had no known properties, but that doesn't seem possible right now.


/** @internal */
export * from "../generated/ts/native.mjs"; // core types are transitively exported.
export * from "realm/binding";
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we just spend a long time figuring out how to make the re-export of every type from core.ts work through native.d.mts, but what if we just ... didn't. Do you think it would actually be cleaner to have this file do the export * from "../generated/ts/core"; rather than doing that in the wrapper and the .d.ts? That might let you remove the virtual realm/binding/core path.

Feel free to ignore if you are just tired of fucking around with that file 😁

import { inject } from "../platform/file-system";
import { extendDebug } from "../debug";
import { inject } from "../file-system";
import { extendDebug } from "../../debug";
Copy link
Contributor

Choose a reason for hiding this comment

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

We should consider defining a virtual path REALM/ or src/ so that imports look the same regardless of where you are in the directory structure. It is really unfortunate that when you move a file around, you need to modify its own imports, not just the imports of it. This is pretty common in C++ projects to always use "fully qualified" paths in #includes, so you use #include "realm/object-store/object.hpp" rather than #include "./object.hpp" if you are in realm/object-store/blah.hpp.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that's a good idea, but I'll leave it for a future PR 👍 Ideally I would also like to find a way to enforce that we import through the internal module instead of relative paths (for all other files than the platform specific).

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this file only used for editors and bundle.d.ts generation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe so.

@kraenhansen kraenhansen force-pushed the kh/binding-resolution branch from af8ec30 to 0dda196 Compare May 11, 2023 14:01
@kraenhansen kraenhansen force-pushed the kh/binding-resolution branch from 0dda196 to 49f3d4e Compare May 11, 2023 17:38
@kraenhansen
Copy link
Member Author

I'll merge now, as the two failing "Test Coverage" scripts seem very unrelated to the change I'm proposing.

@kraenhansen kraenhansen merged commit 6993caf into main May 15, 2023
@kraenhansen kraenhansen deleted the kh/binding-resolution branch May 15, 2023 11:12
kraenhansen added a commit that referenced this pull request May 16, 2023
Follow-up to #5802 - we shouldn't use declare in a "declare module" block.
papafe added a commit that referenced this pull request May 17, 2023
* main:
  Pushing update to the package-lock.json
  Binding resolution refactored (#5802)
  Update install-test-react-native.yml (#5808)
  Add more testing of IN operator (#5809)
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants