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

@location tag #3011

Merged
merged 10 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/two-melons-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@quri/squiggle-lang": patch
"@quri/squiggle-components": patch
---

Adds new @location tag and Tag.getLocation
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ export function useScrollToEditorPath(path: SqValuePath) {
return () => {
if (editor) {
const value = findNode(path)?.value();
const location = value?.context?.findLocation();
const taggedLocation = value?.tags.location();
const location = taggedLocation || value?.context?.findLocation();

if (location) {
editor?.scrollTo(location.start.offset, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ fn = {|e| e}
bar = [x, fn]

@startOpen
@location
s = 4 to 10

@startClosed
Expand Down
10 changes: 10 additions & 0 deletions packages/squiggle-lang/__tests__/library/tag_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ describe("Tags", () => {
testEvalToBe("3 -> Tag.hide -> Tag.getHide", "true");
});

describe("location", () => {
testEvalToBe(
`@location
a = 3
Tag.getLocation(a)
`,
'{source: "main", start: {line: 2, column: 1, offset: 10}, end: {line: 2, column: 6, offset: 15}}'
);
});

describe("startOpenToggle", () => {
testEvalToBe("3 -> Tag.startOpen -> Tag.getStartOpenState", '"open"');
testEvalToBe("3 -> Tag.startClosed -> Tag.getStartOpenState", '"closed"');
Expand Down
36 changes: 34 additions & 2 deletions packages/squiggle-lang/src/fr/tag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { REArgumentError } from "../errors/messages.js";
import { REArgumentError, REOther } from "../errors/messages.js";
import { makeFnExample } from "../library/registry/core.js";
import { makeDefinition } from "../library/registry/fnDefinition.js";
import {
Expand Down Expand Up @@ -31,6 +31,7 @@ import { Lambda } from "../reducer/lambda.js";
import { getOrThrow } from "../utility/result.js";
import { Value } from "../value/index.js";
import { ValueTags, ValueTagsType } from "../value/valueTags.js";
import { ValueTagsWrapper } from "../value/ValueTagsWrapper.js";
import { vBool, VBool } from "../value/VBool.js";
import { vString } from "../value/VString.js";

Expand Down Expand Up @@ -389,13 +390,44 @@ example2 = {|x| x + 1}`,
}),
],
}),
maker.make({
name: "location",
description: `Saves the location of a value. Note that this must be called at the point where the location is to be saved. If you use it in a helper function, it will save the location of the helper function, not the location where the helper function is called.`,
displaySection: "Tags",
definitions: [
makeDefinition(
[frWithTags(frAny({ genericName: "A" }))],
frWithTags(frAny({ genericName: "A" })),
([{ value, tags }], { frameStack }) => {
const location = frameStack.getTopFrame()?.location;
if (!location) {
throw new REOther("Location is missing in call stack");
}
return {
value,
tags: tags.merge({ location: location }),
};
},
{ isDecorator: true }
),
],
}),
maker.make({
name: "getLocation",
displaySection: "Tags",
definitions: [
makeDefinition([frWithTags(frAny())], frAny(), ([{ tags }]) => {
return new ValueTagsWrapper(tags).location() || vString("None");
}),
],
}),
maker.make({
name: "getAll",
displaySection: "Functions",
description: "Returns a dictionary of all tags on a value.",
definitions: [
makeDefinition([frAny()], frDictWithArbitraryKeys(frAny()), ([value]) => {
return value.getTags().toMap();
return new ValueTagsWrapper(value.getTags()).toMap();
}),
],
}),
Expand Down
6 changes: 6 additions & 0 deletions packages/squiggle-lang/src/public/SqValue/SqTags.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LocationRange } from "peggy";

import { ValueTags } from "../../value/valueTags.js";
import { SqValueContext } from "../SqValueContext.js";
import { SqValue, wrapValue } from "./index.js";
Expand Down Expand Up @@ -40,4 +42,8 @@ export class SqTags {
startOpenState(): "open" | "closed" | undefined {
return this.tags.startOpenState();
}

location(): LocationRange | undefined {
return this.tags.location();
}
}
85 changes: 85 additions & 0 deletions packages/squiggle-lang/src/value/ValueTagsWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Location, LocationRange } from "peggy";

import { ImmutableMap } from "../utility/immutableMap.js";
import { Value, vNumber, vString } from "./index.js";
import { ValueTags } from "./valueTags.js";
import { vDict, type VDict } from "./VDict.js";

function locationRangeToValue(locationRange: LocationRange) {
function locationToDict(location: Location): VDict {
return vDict(
ImmutableMap([
["line", vNumber(location.line)],
["column", vNumber(location.column)],
["offset", vNumber(location.offset)],
])
);
}
let items: [string, Value][] = [
["start", locationToDict(locationRange.start)],
["end", locationToDict(locationRange.end)],
];
if (typeof locationRange.source === "string") {
items = [["source", vString(locationRange.source)], ...items];
}
return vDict(ImmutableMap(items));
}

// This class is meant for functions that depend on Value. BaseValue depends on ValueTags, so we can't call many functions there. Instead, we call them here.
export class ValueTagsWrapper {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you could safely skip OOP here and use a module-as-plain-object:

export const tagUtils = {
  toVDict: (tags: ValueTags) => { ... },
  toValueMap: (tags: ValueTags) => { ... },
  toLocation: (tags: ValueTags) => { ... },
}

And then return tagUtils.toValueMap(tags).

(AFAICT the only function you need here is toValueMap, but exporting many small functions one by one can be annoying and less readable, so a top-level object is more convenient)

constructor(public value: ValueTags) {}

toList(): [string, Value][] {
const result: [string, Value][] = [];
const { value } = this.value;
if (value.name?.value !== undefined) {
result.push(["name", value.name]);
}
if (value.doc?.value !== undefined) {
result.push(["doc", value.doc]);
}
if (value.showAs !== undefined) {
result.push(["showAs", value.showAs]);
}
if (value.numberFormat !== undefined) {
result.push(["numberFormat", value.numberFormat]);
}
if (value.dateFormat !== undefined) {
result.push(["dateFormat", value.dateFormat]);
}
if (value.hidden !== undefined) {
result.push(["hidden", value.hidden]);
}
if (value.notebook !== undefined) {
result.push(["notebook", value.notebook]);
}
const _exportData = this.value.exportData();
if (_exportData !== undefined) {
result.push(["exportData", _exportData]);
}

if (value.startOpenState !== undefined) {
result.push(["startOpenState", value.startOpenState]);
}
if (value.location !== undefined) {
result.push(["location", locationRangeToValue(value.location)]);
}

return result;
}

toMap(): ImmutableMap<string, Value> {
return ImmutableMap(this.toList());
}

toString(): string {
return this.toList()
.map(([key, value]) => `${key}: ${value.toString()}`)
.join(", ");
}

location(): VDict | undefined {
const _location = this.value.location();
return _location ? locationRangeToValue(_location) : undefined;
}
}
73 changes: 26 additions & 47 deletions packages/squiggle-lang/src/value/valueTags.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ImmutableMap } from "../utility/immutableMap.js";
import { LocationRange } from "peggy";

import { Err, fmap, mergeMany, Ok, result } from "../utility/result.js";
import { Value } from "./index.js";
import { type VBool } from "./VBool.js";
import { type VDict } from "./VDict.js";
import { VDict } from "./VDict.js";
import { type VString } from "./VString.js";

// Note: this file can't call any `vType` constructors; it would cause a circular dependency because of `BaseValue` -> `ValueTags`.
Expand All @@ -18,6 +19,7 @@ export type ValueTagsType = {
notebook?: VBool; // can be set on arrays
exportData?: VDict; // should be { sourceId: String, path: List(String) }
startOpenState?: VString;
location?: LocationRange;
};

type ValueTagsTypeName = keyof ValueTagsType;
Expand All @@ -32,6 +34,7 @@ const valueTagsTypeNames: ValueTagsTypeName[] = [
"notebook",
"exportData",
"startOpenState",
"location",
];

function convertToValueTagsTypeName(
Expand All @@ -54,43 +57,25 @@ function convertToValueTagsTypeName(
export class ValueTags {
constructor(public value: ValueTagsType) {}

toList(): [string, Value][] {
const result: [string, Value][] = [];
const { value } = this;
if (value.name?.value) {
result.push(["name", value.name]);
}
if (value.doc?.value) {
result.push(["doc", value.doc]);
}
if (value.showAs) {
result.push(["showAs", value.showAs]);
}
if (value.numberFormat) {
result.push(["numberFormat", value.numberFormat]);
}
if (value.dateFormat) {
result.push(["dateFormat", value.dateFormat]);
}
if (value.hidden) {
result.push(["hidden", value.hidden]);
}
if (value.notebook) {
result.push(["notebook", value.notebook]);
}
const _exportData = this.exportData();
if (_exportData) {
result.push(["exportData", _exportData]);
}

if (value.startOpenState) {
result.push(["startOpenState", value.startOpenState]);
}
return result;
isEmpty() {
return (
this.value.name === undefined &&
this.value.doc === undefined &&
this.value.showAs === undefined &&
this.value.numberFormat === undefined &&
this.value.dateFormat === undefined &&
this.value.hidden === undefined &&
this.value.notebook === undefined &&
this.value.exportData === undefined &&
this.value.startOpenState === undefined &&
this.value.location === undefined
);
}

isEmpty() {
return this.toList().length === 0;
toString(): string {
return Object.entries(this.value)
.map(([key, value]) => `${key}: ${value?.toString()}`)
.join(", ");
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could also write this out. The previous setup would also work, though it's nice to organize it in ValueTagsWrapper, which can do a better job. (It can convert to Values, for use with the Library)


omit(keys: ValueTagsTypeName[]) {
Expand All @@ -105,16 +90,6 @@ export class ValueTags {
return fmap(params, (args) => this.omit(args));
}

toMap(): ImmutableMap<string, Value> {
return ImmutableMap(this.toList());
}

toString(): string {
return this.toList()
.map(([key, value]) => `${key}: ${value.toString()}`)
.join(", ");
}

merge(other: ValueTagsType) {
return new ValueTags({
...this.value,
Expand Down Expand Up @@ -150,6 +125,10 @@ export class ValueTags {
return this.value.notebook?.value;
}

location() {
return this.value.location;
}

startOpenState(): "open" | "closed" | undefined {
const { value } = this.value.startOpenState ?? {};
if (!value) {
Expand Down