Skip to content

Commit

Permalink
Ast.Vector (#9328)
Browse files Browse the repository at this point in the history
Add `Vector` AST type, corresponding to the `RawAst.Tree.Array` type (name `Array` not used for obvious reasons).

This is the first step of #5138.

### Important Notes

- Switched some string-based vector construction to `Vector.new`, improving type-safety.
- The `Ast` changes are covered by the round-trip tests; the use-site changes have been tested manually.
  • Loading branch information
kazcw authored Mar 8, 2024
1 parent 2df2d95 commit 8e437fa
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 83 deletions.
15 changes: 15 additions & 0 deletions app/gui2/shared/ast/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
PropertyAccess,
TextLiteral,
UnaryOprApp,
Vector,
Wildcard,
} from './tree'

Expand Down Expand Up @@ -290,6 +291,20 @@ class Abstractor {
node = Import.concrete(this.module, polyglot, from, import_, all, as, hiding)
break
}
case RawAst.Tree.Type.Array: {
const left = this.abstractToken(tree.left)
const elements = []
if (tree.first) elements.push({ value: this.abstractTree(tree.first) })
for (const rawElement of tree.rest) {
elements.push({
delimiter: this.abstractToken(rawElement.operator),
value: rawElement.body && this.abstractTree(rawElement.body),
})
}
const right = this.abstractToken(tree.right)
node = Vector.concrete(this.module, left, elements, right)
break
}
default: {
node = Generic.concrete(this.module, this.abstractChildren(tree))
}
Expand Down
213 changes: 168 additions & 45 deletions app/gui2/shared/ast/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ type StructuralField<T extends TreeRefs = RawRefs> =
| NameSpecification<T>
| TextElement<T>
| ArgumentDefinition<T>
| VectorElement<T>

/** Type whose fields are all suitable for storage as `Ast` fields. */
interface FieldObject<T extends TreeRefs> {
Expand Down Expand Up @@ -470,6 +471,10 @@ function mapRefs<T extends TreeRefs, U extends TreeRefs>(
field: ArgumentDefinition<T>,
f: MapRef<T, U>,
): ArgumentDefinition<U>
function mapRefs<T extends TreeRefs, U extends TreeRefs>(
field: VectorElement<T>,
f: MapRef<T, U>,
): VectorElement<U>
function mapRefs<T extends TreeRefs, U extends TreeRefs>(
field: FieldData<T>,
f: MapRef<T, U>,
Expand Down Expand Up @@ -646,6 +651,9 @@ function ensureUnspaced<T>(child: NodeChild<T>, verbatim: boolean | undefined):
function preferUnspaced<T>(child: NodeChild<T>): NodeChild<T> {
return child.whitespace === undefined ? { whitespace: '', ...child } : child
}
function preferSpaced<T>(child: NodeChild<T>): NodeChild<T> {
return child.whitespace === undefined ? { whitespace: ' ', ...child } : child
}
export class MutableApp extends App implements MutableAst {
declare readonly module: MutableModule
declare readonly fields: FixedMap<AstFields & AppFields>
Expand Down Expand Up @@ -1335,7 +1343,7 @@ export class TextLiteral extends Ast {
if (!(parsed instanceof MutableTextLiteral)) {
console.error(`Failed to escape string for interpolated text`, rawText, escaped, parsed)
const safeText = rawText.replaceAll(/[^-+A-Za-z0-9_. ]/g, '')
return this.new(safeText, module)
return TextLiteral.new(safeText, module)
}
return parsed
}
Expand Down Expand Up @@ -2137,6 +2145,116 @@ export class MutableWildcard extends Wildcard implements MutableAst {
export interface MutableWildcard extends Wildcard, MutableAst {}
applyMixins(MutableWildcard, [MutableAst])

type AbstractVectorElement<T extends TreeRefs> = {
delimiter?: T['token']
value: T['ast'] | undefined
}
function delimitVectorElement(element: AbstractVectorElement<OwnedRefs>): VectorElement<OwnedRefs> {
return {
...element,
delimiter: element.delimiter ?? unspaced(Token.new(',', RawAst.Token.Type.Operator)),
}
}
type VectorElement<T extends TreeRefs> = { delimiter: T['token']; value: T['ast'] | undefined }
interface VectorFields {
open: NodeChild<Token>
elements: VectorElement<RawRefs>[]
close: NodeChild<Token>
}
export class Vector extends Ast {
declare fields: FixedMapView<AstFields & VectorFields>
constructor(module: Module, fields: FixedMapView<AstFields & VectorFields>) {
super(module, fields)
}

static concrete(
module: MutableModule,
open: NodeChild<Token> | undefined,
elements: AbstractVectorElement<OwnedRefs>[],
close: NodeChild<Token> | undefined,
) {
const base = module.baseObject('Vector')
const id_ = base.get('id')
const fields = composeFieldData(base, {
open: open ?? unspaced(Token.new('[', RawAst.Token.Type.OpenSymbol)),
elements: elements.map(delimitVectorElement).map((e) => mapRefs(e, ownedToRaw(module, id_))),
close: close ?? unspaced(Token.new(']', RawAst.Token.Type.CloseSymbol)),
})
return asOwned(new MutableVector(module, fields))
}

static new(module: MutableModule, elements: Owned[]) {
return this.concrete(
module,
undefined,
elements.map((value) => ({ value: autospaced(value) })),
undefined,
)
}

static tryBuild<T>(
inputs: Iterable<T>,
elementBuilder: (input: T, module: MutableModule) => Owned,
edit?: MutableModule,
): Owned<MutableVector>
static tryBuild<T>(
inputs: Iterable<T>,
elementBuilder: (input: T, module: MutableModule) => Owned | undefined,
edit?: MutableModule,
): Owned<MutableVector> | undefined
static tryBuild<T>(
inputs: Iterable<T>,
valueBuilder: (input: T, module: MutableModule) => Owned | undefined,
edit?: MutableModule,
): Owned<MutableVector> | undefined {
const module = edit ?? MutableModule.Transient()
const elements = new Array<AbstractVectorElement<OwnedRefs>>()
for (const input of inputs) {
const value = valueBuilder(input, module)
if (!value) return
elements.push({ value: autospaced(value) })
}
return Vector.concrete(module, undefined, elements, undefined)
}

static build<T>(
inputs: Iterable<T>,
elementBuilder: (input: T, module: MutableModule) => Owned,
edit?: MutableModule,
): Owned<MutableVector> {
return Vector.tryBuild(inputs, elementBuilder, edit)
}

*concreteChildren(_verbatim?: boolean): IterableIterator<RawNodeChild> {
const { open, elements, close } = getAll(this.fields)
yield unspaced(open.node)
let isFirst = true
for (const { delimiter, value } of elements) {
if (isFirst && value) {
yield preferUnspaced(value)
} else {
yield delimiter
if (value) yield preferSpaced(value)
}
isFirst = false
}
yield preferUnspaced(close)
}

*values(): IterableIterator<Ast> {
for (const element of this.fields.get('elements'))
if (element.value) yield this.module.get(element.value.node)
}
}
export class MutableVector extends Vector implements MutableAst {
declare readonly module: MutableModule
declare readonly fields: FixedMap<AstFields & VectorFields>
}
export interface MutableVector extends Vector, MutableAst {
values(): IterableIterator<MutableAst>
}
applyMixins(MutableVector, [MutableAst])

export type Mutable<T extends Ast = Ast> =
T extends App ? MutableApp
: T extends Assignment ? MutableAssignment
Expand All @@ -2154,6 +2272,7 @@ export type Mutable<T extends Ast = Ast> =
: T extends PropertyAccess ? MutablePropertyAccess
: T extends TextLiteral ? MutableTextLiteral
: T extends UnaryOprApp ? MutableUnaryOprApp
: T extends Vector ? MutableVector
: T extends Wildcard ? MutableWildcard
: MutableAst

Expand All @@ -2163,36 +2282,38 @@ export function materializeMutable(module: MutableModule, fields: FixedMap<AstFi
switch (type) {
case 'App':
return new MutableApp(module, fieldsForType)
case 'UnaryOprApp':
return new MutableUnaryOprApp(module, fieldsForType)
case 'NegationApp':
return new MutableNegationApp(module, fieldsForType)
case 'OprApp':
return new MutableOprApp(module, fieldsForType)
case 'PropertyAccess':
return new MutablePropertyAccess(module, fieldsForType)
case 'Assignment':
return new MutableAssignment(module, fieldsForType)
case 'BodyBlock':
return new MutableBodyBlock(module, fieldsForType)
case 'Documented':
return new MutableDocumented(module, fieldsForType)
case 'Function':
return new MutableFunction(module, fieldsForType)
case 'Generic':
return new MutableGeneric(module, fieldsForType)
case 'Group':
return new MutableGroup(module, fieldsForType)
case 'Ident':
return new MutableIdent(module, fieldsForType)
case 'Import':
return new MutableImport(module, fieldsForType)
case 'TextLiteral':
return new MutableTextLiteral(module, fieldsForType)
case 'Documented':
return new MutableDocumented(module, fieldsForType)
case 'Invalid':
return new MutableInvalid(module, fieldsForType)
case 'Group':
return new MutableGroup(module, fieldsForType)
case 'NegationApp':
return new MutableNegationApp(module, fieldsForType)
case 'NumericLiteral':
return new MutableNumericLiteral(module, fieldsForType)
case 'Function':
return new MutableFunction(module, fieldsForType)
case 'Assignment':
return new MutableAssignment(module, fieldsForType)
case 'BodyBlock':
return new MutableBodyBlock(module, fieldsForType)
case 'Ident':
return new MutableIdent(module, fieldsForType)
case 'OprApp':
return new MutableOprApp(module, fieldsForType)
case 'PropertyAccess':
return new MutablePropertyAccess(module, fieldsForType)
case 'TextLiteral':
return new MutableTextLiteral(module, fieldsForType)
case 'UnaryOprApp':
return new MutableUnaryOprApp(module, fieldsForType)
case 'Vector':
return new MutableVector(module, fieldsForType)
case 'Wildcard':
return new MutableWildcard(module, fieldsForType)
}
Expand All @@ -2205,36 +2326,38 @@ export function materialize(module: Module, fields: FixedMapView<AstFields>): As
switch (type) {
case 'App':
return new App(module, fields_)
case 'UnaryOprApp':
return new UnaryOprApp(module, fields_)
case 'NegationApp':
return new NegationApp(module, fields_)
case 'OprApp':
return new OprApp(module, fields_)
case 'PropertyAccess':
return new PropertyAccess(module, fields_)
case 'Assignment':
return new Assignment(module, fields_)
case 'BodyBlock':
return new BodyBlock(module, fields_)
case 'Documented':
return new Documented(module, fields_)
case 'Function':
return new Function(module, fields_)
case 'Generic':
return new Generic(module, fields_)
case 'Group':
return new Group(module, fields_)
case 'Ident':
return new Ident(module, fields_)
case 'Import':
return new Import(module, fields_)
case 'TextLiteral':
return new TextLiteral(module, fields_)
case 'Documented':
return new Documented(module, fields_)
case 'Invalid':
return new Invalid(module, fields_)
case 'Group':
return new Group(module, fields_)
case 'NegationApp':
return new NegationApp(module, fields_)
case 'NumericLiteral':
return new NumericLiteral(module, fields_)
case 'Function':
return new Function(module, fields_)
case 'Assignment':
return new Assignment(module, fields_)
case 'BodyBlock':
return new BodyBlock(module, fields_)
case 'Ident':
return new Ident(module, fields_)
case 'OprApp':
return new OprApp(module, fields_)
case 'PropertyAccess':
return new PropertyAccess(module, fields_)
case 'TextLiteral':
return new TextLiteral(module, fields_)
case 'UnaryOprApp':
return new UnaryOprApp(module, fields_)
case 'Vector':
return new Vector(module, fields_)
case 'Wildcard':
return new Wildcard(module, fields_)
}
Expand Down
2 changes: 1 addition & 1 deletion app/gui2/src/components/GraphEditor/GraphEdges.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ function disconnectEdge(target: PortId) {
function createEdge(source: AstId, target: PortId) {
const ident = graph.db.getOutputPortIdentifier(source)
if (ident == null) return
const identAst = Ast.parse(ident)
const sourceNode = graph.db.getPatternExpressionNodeId(source)
const targetNode = graph.getPortNodeId(target)
Expand All @@ -89,6 +88,7 @@ function createEdge(source: AstId, target: PortId) {
// Creating this edge would create a circular dependency. Prevent that and display error.
toast.error('Could not connect due to circular dependency.')
} else {
const identAst = Ast.parse(ident, edit)
if (!graph.updatePortValue(edit, target, identAst)) {
if (isAstId(target)) {
console.warn(`Failed to connect edge to port ${target}, falling back to direct edit.`)
Expand Down
11 changes: 4 additions & 7 deletions app/gui2/src/components/GraphEditor/widgets/WidgetFunction.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@ const innerInput = computed(() => {
}
})
const escapeString = (str: string): string => {
const escaped = str.replaceAll(/([\\'])/g, '\\$1')
return `'${escaped}'`
}
const makeArgsList = (args: string[]) => '[' + args.map(escapeString).join(', ') + ']'
const selfArgumentExternalId = computed<Opt<ExternalId>>(() => {
const analyzed = interpretCall(props.input.value, true)
if (analyzed.kind === 'infix') {
Expand Down Expand Up @@ -136,7 +130,10 @@ const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() =>
definedOnType: 'Standard.Visualization.Widgets',
name: 'get_widget_json',
},
positionalArgumentsExpressions: [`.${name}`, makeArgsList(args)],
positionalArgumentsExpressions: [
`.${name}`,
Ast.Vector.build(args, Ast.TextLiteral.new).code(),
],
}
})
Expand Down
5 changes: 2 additions & 3 deletions app/gui2/src/components/GraphEditor/widgets/WidgetText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ const graph = useGraphStore()
const inputTextLiteral = computed((): Ast.TextLiteral | undefined => {
if (props.input.value instanceof Ast.TextLiteral) return props.input.value
const valueStr = WidgetInput.valueRepr(props.input)
const parsed = valueStr != null ? Ast.parse(valueStr) : undefined
if (parsed instanceof Ast.TextLiteral) return parsed
return undefined
if (valueStr == null) return undefined
return Ast.TextLiteral.tryParse(valueStr)
})
function makeNewLiteral(value: string) {
Expand Down
Loading

0 comments on commit 8e437fa

Please sign in to comment.