Skip to content

Commit 51b80a3

Browse files
committed
Make opaque types easier to construct
1 parent 2c79a4a commit 51b80a3

File tree

2 files changed

+89
-81
lines changed

2 files changed

+89
-81
lines changed

src/language/semantics/type-system/prelude-types.ts

Lines changed: 11 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { option as optionADT } from '../../../adts.js'
12
import {
23
makeFunctionType,
34
makeObjectType,
4-
makeOpaqueType,
5+
makeOpaqueStringType,
56
makeUnionType,
6-
matchTypeFormat,
77
type FunctionType,
88
type Type,
99
type UnionType,
@@ -16,77 +16,17 @@ export const nullType = makeUnionType('null', ['null'])
1616

1717
export const boolean = makeUnionType('boolean', ['false', 'true'])
1818

19-
export const string = makeOpaqueType('string', {
20-
isAssignableFrom: source =>
21-
matchTypeFormat(source, {
22-
// functions cannot be assigned to `string`
23-
function: _ => false,
24-
// `string` can't have object types assigned to it
25-
object: _source => false,
26-
// the only opaque subtypes of `string` are `naturalNumber` and itself
27-
opaque: source => source === string || source === naturalNumber,
28-
parameter: source =>
29-
string.isAssignableFrom(source.constraint.assignableTo),
30-
// `string` can have a union assigned to it if all of its members can be assigned to it
31-
union: source => {
32-
for (const sourceMember of source.members) {
33-
if (
34-
typeof sourceMember !== 'string' &&
35-
!string.isAssignableFrom(sourceMember)
36-
) {
37-
return false
38-
}
39-
}
40-
return true
41-
},
42-
}),
43-
isAssignableTo: target =>
44-
matchTypeFormat(target, {
45-
// `string` cannot be assigned to a function type
46-
function: _ => false,
47-
// `string` can't be assigned to object types
48-
object: _target => false,
49-
// `string` (currently) has no opaque supertypes (its only supertype is itself)
50-
opaque: target => target === string,
51-
parameter: target => target.constraint.assignableTo === string,
52-
// `string` can only be assigned to a union type if `string` is one of its members
53-
union: target => target.members.has(string),
54-
}),
19+
export const string = makeOpaqueStringType('string', {
20+
isAssignableFromLiteralType: (_literalType: string) => true,
21+
nearestOpaqueAssignableFrom: () => optionADT.makeSome(naturalNumber),
22+
nearestOpaqueAssignableTo: () => optionADT.none,
5523
})
5624

57-
export const naturalNumber = makeOpaqueType('natural_number', {
58-
isAssignableFrom: source =>
59-
matchTypeFormat(source, {
60-
function: _ => false,
61-
object: _source => false,
62-
// `naturalNumber` (currently) has no opaque subtypes (its only subtype is itself)
63-
opaque: source => source === naturalNumber,
64-
parameter: source =>
65-
naturalNumber.isAssignableFrom(source.constraint.assignableTo),
66-
// `naturalNumber` can have a union assigned to it if all of its members can be assigned to it
67-
union: source => {
68-
for (const sourceMember of source.members) {
69-
if (typeof sourceMember === 'string') {
70-
if (!/(?:0|[1-9](?:[0-9])*)+/.test(sourceMember)) {
71-
return false
72-
}
73-
} else if (!naturalNumber.isAssignableFrom(sourceMember)) {
74-
return false
75-
}
76-
}
77-
return true
78-
},
79-
}),
80-
isAssignableTo: target =>
81-
matchTypeFormat(target, {
82-
function: _ => false,
83-
object: _target => false,
84-
opaque: target => target === naturalNumber || target === string,
85-
parameter: target => target.constraint.assignableTo === naturalNumber,
86-
// `naturalNumber` can only be assigned to a union type if `naturalNumber` is one of its members
87-
union: target =>
88-
target.members.has(naturalNumber) || target.members.has(string),
89-
}),
25+
export const naturalNumber = makeOpaqueStringType('natural_number', {
26+
isAssignableFromLiteralType: literalType =>
27+
/(?:0|[1-9](?:[0-9])*)+/.test(literalType),
28+
nearestOpaqueAssignableFrom: () => optionADT.none,
29+
nearestOpaqueAssignableTo: () => optionADT.makeSome(string),
9030
})
9131

9232
export const object = makeObjectType('object', {})

src/language/semantics/type-system/type-formats.ts

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { option } from '../../../adts.js'
2+
import type { None, Some } from '../../../adts/option.js'
13
import type { Atom } from '../../parsing.js'
24

35
export type FunctionType = {
@@ -44,17 +46,83 @@ export type OpaqueType = {
4446
readonly isAssignableTo: (target: Type) => boolean
4547
}
4648

47-
export const makeOpaqueType = (
49+
// TODO: Opaque object/function types?
50+
export const makeOpaqueStringType = (
4851
name: string,
49-
computations: {
50-
readonly isAssignableFrom: (source: Type) => boolean
51-
readonly isAssignableTo: (target: Type) => boolean
52-
},
53-
): OpaqueType => ({
54-
name,
55-
kind: 'opaque',
56-
...computations,
57-
})
52+
subtyping: {
53+
readonly isAssignableFromLiteralType: (literalType: string) => boolean
54+
} & (
55+
| {
56+
readonly nearestOpaqueAssignableFrom: () => None
57+
readonly nearestOpaqueAssignableTo: () => Some<OpaqueType>
58+
}
59+
| {
60+
readonly nearestOpaqueAssignableFrom: () => Some<OpaqueType>
61+
readonly nearestOpaqueAssignableTo: () => None
62+
}
63+
| {
64+
readonly nearestOpaqueAssignableFrom: () => Some<OpaqueType>
65+
readonly nearestOpaqueAssignableTo: () => Some<OpaqueType>
66+
}
67+
),
68+
): OpaqueType => {
69+
const self: OpaqueType = {
70+
name,
71+
kind: 'opaque',
72+
isAssignableFrom: source =>
73+
matchTypeFormat(source, {
74+
function: _ => false,
75+
object: _ => false,
76+
opaque: source =>
77+
source === self ||
78+
option.match(subtyping.nearestOpaqueAssignableFrom(), {
79+
none: () => false,
80+
some: nearestOpaqueAssignableFrom =>
81+
nearestOpaqueAssignableFrom.isAssignableFrom(source),
82+
}),
83+
parameter: source =>
84+
self.isAssignableFrom(source.constraint.assignableTo),
85+
union: source => {
86+
for (const sourceMember of source.members) {
87+
if (typeof sourceMember === 'string') {
88+
if (!subtyping.isAssignableFromLiteralType(sourceMember)) {
89+
return false
90+
}
91+
} else if (!self.isAssignableFrom(sourceMember)) {
92+
return false
93+
}
94+
}
95+
return true
96+
},
97+
}),
98+
isAssignableTo: target =>
99+
matchTypeFormat(target, {
100+
function: _ => false,
101+
object: _ => false,
102+
opaque: target =>
103+
target === self ||
104+
option.match(subtyping.nearestOpaqueAssignableTo(), {
105+
none: () => false,
106+
some: nearestOpaqueAssignableTo =>
107+
nearestOpaqueAssignableTo.isAssignableTo(target),
108+
}),
109+
parameter: _ => false,
110+
union: target => {
111+
for (const targetMember of target.members) {
112+
if (
113+
// Opaque types are never assignable to literal types.
114+
typeof targetMember !== 'string' &&
115+
self.isAssignableTo(targetMember)
116+
) {
117+
return true
118+
}
119+
}
120+
return false
121+
},
122+
}),
123+
}
124+
return self
125+
}
58126

59127
export type TypeParameter = {
60128
readonly name: string

0 commit comments

Comments
 (0)