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

Support @as("foo") to customize the representation of tags. #6095

Merged
merged 13 commits into from
Mar 27, 2023

Conversation

cristianoc
Copy link
Collaborator

@cristianoc cristianoc commented Mar 23, 2023

module CustomizeTags = {
  type t = | @as("dd") A | @as(12) B | C | @as("qq") D(int) | @as(42) E(int) | F(string)

  let foo = x =>
    switch x {
    | A => 1
    | B => 2
    | C => 3
    | D(_) => 4
    | E(_) => 5
    | F(_) => 6
    }

  let a = A
  let b = B
  let c = C
  let d = D(42)
  let e = E(0)
}

module MyUndefined = {
  type t<'a> = | @as(undefined) Undefined | @as(unboxed) Present('a)
  // Note: 'a must not have undefined as value
  // There can be only one with payload, with 1 argument, to use unboxed

  let undefined = Undefined

  let isUndefined = x => x == Undefined

  let plus = (x, y) =>
    switch (x, y) {
    | (Undefined, _) => y
    | (_, Undefined) => x
    | (Present(n), Present(m)) => Present(n + m)
    }
}

module MyNull = {
  type t<'a> = | @as(null) Null | @as(unboxed) Present('a)
  // Note: 'a must not have null as value
  // There can be only one with payload, with 1 argument, to use unboxed

  let null = Null

  let isNull = x => x == Null

  let plus = (x, y) =>
    switch (x, y) {
    | (Null, _) => y
    | (_, Null) => x
    | (Present(n), Present(m)) => Present(n + m)
    }
}

module MyNullable = {
  type t<'a> =
    | @as(null) Null
    | @as(undefined) Undefined
    | @as(unboxed) Present('a)
  // Note: 'a must not have null or undefined as value
  // There can be only one with payload, with 1 argument, to use unboxed

  let null = Null
  let undefined = Undefined

  let isNull = x => x == Null
  let isUndefined = x => x == Undefined

  let plus = (x, y) =>
    switch (x, y) {
    | (Null | Undefined, _) => y
    | (_, Null | Undefined) => x
    | (Present(x), Present(y)) => Present(x + y)
    }

  let kind = x =>
    switch x {
    | Null => "null"
    | Undefined => "undefined"
    | Present(_) => "present"
    }

  let expectSeven = plus(Present(3), Present(4))
  Js.log2("expect 7:", expectSeven)
}

module MyNullableExtended = {
  type t<'a> =
    | @as(null) Null
    | @as(undefined) Undefined
    | @as(unboxed) Present('a)
    | WhyNotAnotherOne
  // Note: 'a must be a not have null or something that's not an object as value
  // There can be only one with payload, with 1 argument, to use unboxed

  let null = Null
  let undefined = Undefined
  let whynot = WhyNotAnotherOne

  let isNull = x => x == Null
  let isUndefined = x => x == Undefined
  let isWhyNot = x => x == WhyNotAnotherOne

  type vector = {x: float, y: float}

  let plus = (x, y) =>
    switch (x, y) {
    | (Null | Undefined, _) => y
    | (_, Null | Undefined) => x
    | (WhyNotAnotherOne, _) | (_, WhyNotAnotherOne) => WhyNotAnotherOne
    | (Present({x: x1, y: y1}), Present({x: x2, y: y2})) => Present({x: x1 +. x2, y: y1 +. y2})
    }

  let kind = x =>
    switch x {
    | Null => "null"
    | Undefined => "undefined"
    | Present(_) => "present"
    | WhyNotAnotherOne => "whynot"
    }

  let expectSeven = plus(Present({x: 4., y: 3.}), Present({x: 3., y: 4.}))
  Js.log2("expect {x:7, y:7}:", expectSeven)
}

module TaggedUnions = {
  /*
  type Circle = {
    kind: 1; // Number literal
    radius: number;
  };

  type Square = {
    kind: "square"; // String literal
    sideLength: number;
  };

  type Rectangle = {
    kind: "rectangle"; // String literal
    width: number;
    height: number;
  };

  type Shape = Circle | Square | Rectangle;

  function area(shape: Shape): number {
    switch (shape.kind) {
      case 1: // Circle
        return Math.PI * shape.radius ** 2;
      case "square": // Square
        return shape.sideLength ** 2;
      case "rectangle": // Rectangle
        return shape.width * shape.height;
      default:
        throw new Error("Invalid shape kind");
    }
  }
*/
  @tag("kind")
  type shape =
    | @as(1) Circle({radius: float})
    | @as("square") Square({sideLength: float})
    | @as("rectangle") Rectangle({width: float, height: float})

  let area = (shape: shape): float => {
    switch shape {
    | Circle({radius}) => Js.Math._PI *. radius ** 2.
    | Square({sideLength}) => sideLength ** 2.
    | Rectangle({width, height}) => width *. height
    }
  }

  let circle = Circle({radius: 10.})
  let square = Square({sideLength: 10.})
}

module CustomTagNotInline = {
  @tag("custom-tag")
  type t = A(int) | B(int)
  let a = A(10)
  let b = B(20)
}

@cristianoc cristianoc force-pushed the emit_tags_as_strings branch from 6e4a73a to 2524fca Compare March 23, 2023 21:58
Base automatically changed from emit_tags_as_strings to master March 24, 2023 15:31
Compile is_tag to `!== "object"` instead of `=== "string"`.
Also the comment is not emitted anymore, since there's always a tag.
Not special casing means that the representation is uniform, and does not change when the type is extended. This is important with zero cost ffi, where the runtime representation is exposed to the user, to reduce possible surprises.
null and undefined can only be applied to cases with no payloads

unboxed can only be applied when there is exactly one case with payloads, and that case takes exactly one argument

Some of those checks are possible statically. Not all of them are implemented.
Some checks cannot if one wants to have user-level nullable, null, undefined types with pattern matching. E.g. null type could take null as an argument.
Copy link
Collaborator Author

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

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

There are a few places where one needs to make sure assert false is not triggered.

| AsInt i -> small_int i
| AsNull -> nil
| AsUndefined -> undefined
| AsUnboxed -> assert false (* Should not emit tags for unboxed *)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: put restriction on the variant definitions allowed, to make sure this never happens.

expression_desc cxt ~level f (Object objs)
let exp = match objs with
| [(_, e)] when as_value = Some AsUnboxed -> e.expression_desc
| _ when as_value = Some AsUnboxed -> assert false (* should not happen *)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: put restriction on the variant definitions allowed, to make sure this never happens.

@cristianoc cristianoc merged commit c73714f into master Mar 27, 2023
@cristianoc cristianoc deleted the variants_as branch March 27, 2023 08:00
@cometkim
Copy link
Member

cometkim commented Apr 4, 2023

WOW, it seems this fully resolves the #5207

@cristianoc
Copy link
Collaborator Author

WOW, it seems this fully resolves the #5207

I think so. Would you have a try and report back?

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.

2 participants