mirror of
https://github.com/roc-lang/roc.git
synced 2024-10-05 06:37:26 +03:00
Start supporting printing unifications
This commit is contained in:
parent
9e055dcf53
commit
87bb6c8437
@ -11,7 +11,7 @@ export default function App() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container w-screen h-screen p-2">
|
||||
<div className="w-screen h-screen p-2 bg-gray-100">
|
||||
<FileInput setResult={setEvents} />
|
||||
<EventsWrapper events={events} />
|
||||
</div>
|
||||
|
@ -2,30 +2,39 @@ import React from "react";
|
||||
import { AllEvents, Event, UnificationMode } from "../schema";
|
||||
import { Refine } from "../utils/refine";
|
||||
import clsx from "clsx";
|
||||
import { Engine, EventIndex } from "../engine/engine";
|
||||
import { lastSubEvent } from "../engine/event_util";
|
||||
import { VariableEl } from "./Variable";
|
||||
|
||||
interface UiProps {
|
||||
events: AllEvents;
|
||||
}
|
||||
|
||||
export default function Ui({ events }: UiProps): JSX.Element {
|
||||
const engine = new Engine(events);
|
||||
|
||||
return (
|
||||
<div className="font-mono">
|
||||
<EventList root events={events}></EventList>
|
||||
<div className="font-mono mt-4">
|
||||
<EventList engine={engine} root events={events}></EventList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventListProps {
|
||||
engine: Engine;
|
||||
events: Event[];
|
||||
root?: boolean;
|
||||
}
|
||||
|
||||
function EventList({ events, root }: EventListProps): JSX.Element {
|
||||
const MT = "mt-2.5";
|
||||
const UNFOCUSED = "opacity-40";
|
||||
|
||||
function EventList({ engine, events, root }: EventListProps): JSX.Element {
|
||||
return (
|
||||
<ul className={clsx(root ? "ml-2 mt-4" : "ml-[1.5em]", "relative")}>
|
||||
<ul className={clsx(MT, root ? "ml-2" : "ml-[1.5em]", "relative")}>
|
||||
{events.map((event, i) => (
|
||||
<li key={i} className="mt-2">
|
||||
<OneEvent event={event} />
|
||||
<li key={i} className={MT}>
|
||||
<OneEvent engine={engine} event={event} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -33,13 +42,14 @@ function EventList({ events, root }: EventListProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface OneEventProps {
|
||||
engine: Engine;
|
||||
event: Event;
|
||||
}
|
||||
|
||||
function OneEvent({ event }: OneEventProps): JSX.Element {
|
||||
function OneEvent({ event, engine }: OneEventProps): JSX.Element {
|
||||
switch (event.type) {
|
||||
case "Unification":
|
||||
return <Unification event={event} />;
|
||||
return <Unification engine={engine} event={event} />;
|
||||
case "VariableUnified":
|
||||
return <></>;
|
||||
case "VariableSetDescriptor":
|
||||
@ -54,47 +64,76 @@ const UN_UNKNOWN = "❔";
|
||||
const UN_SUCCESS = "✅";
|
||||
const UN_FAILURE = "❌";
|
||||
|
||||
function Unification({
|
||||
event,
|
||||
}: {
|
||||
interface UnificationProps {
|
||||
engine: Engine;
|
||||
event: Refine<Event, "Unification">;
|
||||
}): JSX.Element {
|
||||
const { left, right, mode, subevents, success } = event;
|
||||
}
|
||||
|
||||
function Unification({ engine, event }: UnificationProps): JSX.Element {
|
||||
const { mode, subevents, success } = event;
|
||||
|
||||
const beforeUnificationIndex = engine.getEventIndex(event);
|
||||
const afterUnificationIndex = engine.getEventIndex(lastSubEvent(event));
|
||||
|
||||
const leftVar = (index: EventIndex) => (
|
||||
<VariableEl engine={engine} index={index} variable={event.left} />
|
||||
);
|
||||
const rightVar = (index: EventIndex) => (
|
||||
<VariableEl engine={engine} index={index} variable={event.right} />
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const result = success ? UN_SUCCESS : UN_FAILURE;
|
||||
const modeIcon = <UnificationModeIcon mode={mode} />;
|
||||
const dropdownIcon = isOpen ? DROPDOWN_OPEN : DROPDOWN_CLOSED;
|
||||
|
||||
const headLineIcon = isOpen ? UN_UNKNOWN : result;
|
||||
|
||||
const headLine = (
|
||||
<button onClick={() => setIsOpen(!isOpen)} className="w-full text-left">
|
||||
<span className="text-slate-400 mr-2">{dropdownIcon}</span>
|
||||
{headLineIcon} {left} {modeIcon} {right}
|
||||
</button>
|
||||
const resultIcon = success ? UN_SUCCESS : UN_FAILURE;
|
||||
const resultHeadline = <Headline icon={resultIcon}></Headline>;
|
||||
const topHeadline = (
|
||||
<Headline icon={isOpen ? UN_UNKNOWN : resultIcon}></Headline>
|
||||
);
|
||||
|
||||
function getHeadline(index: EventIndex) {
|
||||
return (
|
||||
<button onClick={() => setIsOpen(!isOpen)} className="w-full text-left">
|
||||
<span className="text-slate-400 mr-2">{dropdownIcon}</span>
|
||||
{topHeadline} {leftVar(index)} {modeIcon} {rightVar(index)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return <div className="opacity-60">{headLine}</div>;
|
||||
const headLine = getHeadline(afterUnificationIndex);
|
||||
return <div className={UNFOCUSED}>{headLine}</div>;
|
||||
} else {
|
||||
const dropdownTransparent = (
|
||||
<span className="text-transparent mr-2">{dropdownIcon}</span>
|
||||
);
|
||||
|
||||
const headlineBefore = getHeadline(beforeUnificationIndex);
|
||||
|
||||
const headlineAfter = (
|
||||
<div className={MT}>
|
||||
{dropdownTransparent}
|
||||
{resultHeadline} {leftVar(afterUnificationIndex)} {modeIcon}{" "}
|
||||
{rightVar(afterUnificationIndex)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{headLine}</div>
|
||||
<EventList events={subevents} />
|
||||
<div className="mt-2">
|
||||
{dropdownTransparent}
|
||||
{result} {left} {modeIcon} {right}
|
||||
</div>
|
||||
<div>{headlineBefore}</div>
|
||||
<EventList engine={engine} events={subevents} />
|
||||
{headlineAfter}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Headline({ icon }: { icon: string }): JSX.Element {
|
||||
return <span className="">{icon}</span>;
|
||||
}
|
||||
|
||||
function UnificationModeIcon({ mode }: { mode: UnificationMode }): JSX.Element {
|
||||
switch (mode.type) {
|
||||
case "Eq":
|
||||
|
92
crates/compiler/checkmate/www/src/components/Variable.tsx
Normal file
92
crates/compiler/checkmate/www/src/components/Variable.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import clsx from "clsx";
|
||||
import { Engine, EventIndex } from "../engine/engine";
|
||||
import { TypeDescriptor } from "../engine/subs";
|
||||
import { Variable } from "../schema";
|
||||
import { assertExhaustive } from "../utils/exhaustive";
|
||||
|
||||
interface VariableProps {
|
||||
engine: Engine;
|
||||
index: EventIndex;
|
||||
variable: Variable;
|
||||
}
|
||||
|
||||
export function VariableEl({
|
||||
engine,
|
||||
index,
|
||||
variable,
|
||||
}: VariableProps): JSX.Element {
|
||||
engine.stepTo(index);
|
||||
const desc = engine.subs.get_root(variable);
|
||||
const { name, bg } = contentStyles(desc);
|
||||
|
||||
return (
|
||||
<span className={clsx("py-0 pl-0 pr-1 rounded-md", bg)}>
|
||||
<span className="ring-1 ring-inset ring-black-100 mr-1 px-1 bg-white rounded-md">
|
||||
{variable}
|
||||
</span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContentStyles {
|
||||
name: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
function contentStyles(desc: TypeDescriptor | undefined): ContentStyles {
|
||||
if (!desc) {
|
||||
return { name: "???", bg: "bg-red-500" };
|
||||
}
|
||||
|
||||
const content = desc.content;
|
||||
switch (content.type) {
|
||||
case "Flex":
|
||||
return { name: "Flex", bg: "bg-blue-300" };
|
||||
case "FlexAble":
|
||||
return { name: "FlexAble", bg: "bg-blue-400" };
|
||||
case "Rigid":
|
||||
return { name: "Rigid", bg: "bg-indigo-300" };
|
||||
case "RigidAble":
|
||||
return { name: "RigidAble", bg: "bg-indigo-400" };
|
||||
case "Recursive":
|
||||
return { name: "Rec", bg: "bg-blue-grey-500" };
|
||||
case "LambdaSet":
|
||||
return { name: "LambdaSet", bg: "bg-green-500" };
|
||||
case "Alias": {
|
||||
switch (content.kind.type) {
|
||||
case "Structural":
|
||||
return { name: "Alias", bg: "bg-yellow-300" };
|
||||
case "Opaque":
|
||||
return { name: "Opaque", bg: "bg-amber-400" };
|
||||
default:
|
||||
assertExhaustive(content.kind);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Apply":
|
||||
return { name: "Apply", bg: "bg-orange-500" };
|
||||
case "Function":
|
||||
return { name: "Func", bg: "bg-teal-400" };
|
||||
case "Record":
|
||||
return { name: "Record", bg: "bg-purple-400" };
|
||||
case "Tuple":
|
||||
return { name: "Tuple", bg: "bg-deep-purple-400" };
|
||||
case "TagUnion":
|
||||
return { name: "Tags", bg: "bg-cyan-200" };
|
||||
case "FunctionOrTagUnion":
|
||||
return { name: "Func|Tags", bg: "bg-cyan-300" };
|
||||
case "RecursiveTagUnion":
|
||||
return { name: "RecTags", bg: "bg-cyan-400" };
|
||||
case "RangedNumber":
|
||||
return { name: "ℕ", bg: "bg-lime-400" };
|
||||
case "EmptyRecord":
|
||||
return { name: "{}", bg: "bg-purple-400" };
|
||||
case "EmptyTuple":
|
||||
return { name: "()", bg: "bg-deep-purple-400" };
|
||||
case "EmptyTagUnion":
|
||||
return { name: "[]", bg: "bg-cyan-200" };
|
||||
case "Error":
|
||||
return { name: "Error", bg: "bg-red-400" };
|
||||
}
|
||||
}
|
142
crates/compiler/checkmate/www/src/engine/engine.tsx
Normal file
142
crates/compiler/checkmate/www/src/engine/engine.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { Event, Variable } from "../schema";
|
||||
import { assertExhaustive } from "../utils/exhaustive";
|
||||
import {
|
||||
ChangeEvent,
|
||||
makeDeleteVariable,
|
||||
makeRevertVariable,
|
||||
RollbackChange,
|
||||
Subs,
|
||||
} from "./subs";
|
||||
|
||||
export type EventIndex = number & { __eventIndex: never };
|
||||
|
||||
function* flattenEvents(events: Event[]): Generator<Event> {
|
||||
for (const event of events) {
|
||||
yield event;
|
||||
switch (event.type) {
|
||||
case "Unification": {
|
||||
yield* flattenEvents(event.subevents);
|
||||
break;
|
||||
}
|
||||
case "VariableUnified":
|
||||
case "VariableSetDescriptor":
|
||||
break;
|
||||
default:
|
||||
assertExhaustive(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFlatEvents(events: Event[]): {
|
||||
flatEvents: Event[];
|
||||
map: Map<Event, EventIndex>;
|
||||
} {
|
||||
const map = new Map<Event, EventIndex>();
|
||||
const flatEvents = Array.from(flattenEvents(events));
|
||||
let i = 0;
|
||||
for (const event of flatEvents) {
|
||||
map.set(event, i as EventIndex);
|
||||
i++;
|
||||
}
|
||||
return { flatEvents, map };
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
#eventIndexMap: Map<Event, EventIndex>;
|
||||
#events: Event[];
|
||||
#subs: Subs = new Subs();
|
||||
#reverseEvents: Map<EventIndex, RollbackChange> = new Map();
|
||||
|
||||
#nextIndexForward: EventIndex = 0 as EventIndex;
|
||||
|
||||
constructor(events: Event[]) {
|
||||
const { flatEvents, map } = getFlatEvents(events);
|
||||
this.#eventIndexMap = map;
|
||||
this.#events = flatEvents;
|
||||
}
|
||||
|
||||
getEventIndex(event: Event): EventIndex {
|
||||
const index = this.#eventIndexMap.get(event);
|
||||
if (index === undefined) {
|
||||
throw new Error("Event not found");
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
get step(): EventIndex {
|
||||
return this.#nextIndexForward;
|
||||
}
|
||||
|
||||
stepTo(eventIndex: EventIndex): void {
|
||||
while (this.#nextIndexForward <= eventIndex) {
|
||||
this.stepForward(this.#nextIndexForward);
|
||||
++this.#nextIndexForward;
|
||||
}
|
||||
while (this.#nextIndexForward > eventIndex) {
|
||||
--this.#nextIndexForward;
|
||||
this.stepBackward(this.#nextIndexForward);
|
||||
}
|
||||
}
|
||||
|
||||
get subs(): Readonly<Subs> {
|
||||
return this.#subs;
|
||||
}
|
||||
|
||||
private stepForward(eventIndex: EventIndex): void {
|
||||
const event = this.#events[eventIndex];
|
||||
if (!isApplicable(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#reverseEvents.has(eventIndex)) {
|
||||
const variable = applicableVariable(event);
|
||||
const current = this.#subs.get(variable);
|
||||
let revert: RollbackChange;
|
||||
if (!current) {
|
||||
revert = makeDeleteVariable({ variable });
|
||||
} else {
|
||||
revert = makeRevertVariable({ variable, to: current });
|
||||
}
|
||||
this.#reverseEvents.set(eventIndex, revert);
|
||||
}
|
||||
|
||||
this.#subs.apply(event);
|
||||
}
|
||||
|
||||
private stepBackward(eventIndex: EventIndex): void {
|
||||
const event = this.#events[eventIndex];
|
||||
if (!isApplicable(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revert = this.#reverseEvents.get(eventIndex);
|
||||
if (!revert) {
|
||||
throw new Error("No revert found");
|
||||
}
|
||||
|
||||
this.#subs.apply(revert);
|
||||
}
|
||||
}
|
||||
|
||||
function isApplicable(event: Event): event is ChangeEvent {
|
||||
switch (event.type) {
|
||||
case "VariableUnified":
|
||||
case "VariableSetDescriptor":
|
||||
return true;
|
||||
case "Unification":
|
||||
return false;
|
||||
default:
|
||||
assertExhaustive(event);
|
||||
}
|
||||
}
|
||||
|
||||
function applicableVariable(event: ChangeEvent): Variable {
|
||||
switch (event.type) {
|
||||
case "VariableUnified":
|
||||
return event.from;
|
||||
case "VariableSetDescriptor":
|
||||
return event.variable;
|
||||
default:
|
||||
assertExhaustive(event);
|
||||
}
|
||||
}
|
19
crates/compiler/checkmate/www/src/engine/event_util.tsx
Normal file
19
crates/compiler/checkmate/www/src/engine/event_util.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Event } from "../schema";
|
||||
|
||||
export function lastSubEvent(event: Event): Event {
|
||||
switch (event.type) {
|
||||
case "Unification": {
|
||||
const subevents = event.subevents;
|
||||
if (subevents.length === 0) {
|
||||
return event;
|
||||
}
|
||||
return lastSubEvent(event.subevents[event.subevents.length - 1]);
|
||||
}
|
||||
case "VariableUnified": {
|
||||
return event;
|
||||
}
|
||||
case "VariableSetDescriptor": {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
}
|
121
crates/compiler/checkmate/www/src/engine/subs.tsx
Normal file
121
crates/compiler/checkmate/www/src/engine/subs.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { Content, Rank, Variable, Event } from "../schema";
|
||||
import { assertExhaustive } from "../utils/exhaustive";
|
||||
import { Refine } from "../utils/refine";
|
||||
|
||||
export type TypeLink = {
|
||||
type: "link";
|
||||
to: Variable;
|
||||
};
|
||||
|
||||
function link({ to }: Omit<TypeLink, "type">): TypeLink {
|
||||
return { type: "link", to };
|
||||
}
|
||||
|
||||
export type TypeDescriptor = {
|
||||
type: "descriptor";
|
||||
rank: Rank;
|
||||
content: Content;
|
||||
};
|
||||
|
||||
function descriptor({
|
||||
rank,
|
||||
content,
|
||||
}: Omit<TypeDescriptor, "type">): TypeDescriptor {
|
||||
return { type: "descriptor", rank, content };
|
||||
}
|
||||
|
||||
export type VarType = TypeLink | TypeDescriptor;
|
||||
|
||||
export type RevertVariableChange = {
|
||||
type: "revertTo";
|
||||
variable: Variable;
|
||||
to: VarType;
|
||||
};
|
||||
|
||||
export type DeleteVariableChange = {
|
||||
type: "delete";
|
||||
variable: Variable;
|
||||
};
|
||||
|
||||
export type RollbackChange = RevertVariableChange | DeleteVariableChange;
|
||||
|
||||
export function makeRevertVariable({
|
||||
variable,
|
||||
to,
|
||||
}: Omit<RevertVariableChange, "type">): RevertVariableChange {
|
||||
return { type: "revertTo", variable, to: { ...to } };
|
||||
}
|
||||
|
||||
export function makeDeleteVariable({
|
||||
variable,
|
||||
}: Omit<DeleteVariableChange, "type">): DeleteVariableChange {
|
||||
return { type: "delete", variable };
|
||||
}
|
||||
|
||||
export type ChangeEvent =
|
||||
| Refine<Event, "VariableUnified">
|
||||
| Refine<Event, "VariableSetDescriptor">;
|
||||
|
||||
export type Change = ChangeEvent | RollbackChange;
|
||||
|
||||
export class Subs {
|
||||
#map: Map<Variable, VarType> = new Map();
|
||||
|
||||
get(variable: Variable): VarType | undefined {
|
||||
return this.#map.get(variable);
|
||||
}
|
||||
|
||||
get_root(variable: Variable): TypeDescriptor | undefined {
|
||||
const type = this.get(variable);
|
||||
if (type === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (type.type) {
|
||||
case "descriptor":
|
||||
return type;
|
||||
case "link":
|
||||
return this.get_root(type.to);
|
||||
default:
|
||||
assertExhaustive(type);
|
||||
}
|
||||
}
|
||||
|
||||
apply(change: Change): void {
|
||||
switch (change.type) {
|
||||
case "VariableUnified": {
|
||||
const { from, to } = change;
|
||||
if (from !== to) {
|
||||
this.#map.set(from, link({ to }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "VariableSetDescriptor": {
|
||||
const { variable, rank, content } = change;
|
||||
const existing = this.get_root(variable);
|
||||
if (existing !== undefined) {
|
||||
const nu = descriptor({ ...existing });
|
||||
if (rank) nu.rank = rank;
|
||||
if (content) nu.content = content;
|
||||
this.#map.set(variable, nu);
|
||||
} else {
|
||||
if (typeof rank !== "number") throw new Error("rank is required");
|
||||
if (!content) throw new Error("content is required");
|
||||
this.#map.set(variable, descriptor({ rank, content }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "revertTo": {
|
||||
const { variable, to } = change;
|
||||
this.#map.set(variable, { ...to });
|
||||
break;
|
||||
}
|
||||
case "delete": {
|
||||
const { variable } = change;
|
||||
this.#map.delete(variable);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assertExhaustive(change);
|
||||
}
|
||||
}
|
||||
}
|
3
crates/compiler/checkmate/www/src/utils/exhaustive.ts
Normal file
3
crates/compiler/checkmate/www/src/utils/exhaustive.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function assertExhaustive(_: never): never {
|
||||
throw new Error("Exhaustive switch");
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
Loading…
Reference in New Issue
Block a user