From 147b5b9ef572b02113d99f3bc1a619da6d722468 Mon Sep 17 00:00:00 2001 From: Michael Toy <66150587+mtoy-googly-moogly@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:42:53 -1000 Subject: [PATCH] allow implies keys in arrys of records --- .../ast/expressions/expr-array-literal.ts | 6 ++- .../ast/expressions/expr-record-literal.ts | 52 ++++++++++++++++--- .../malloy/src/lang/grammar/MalloyParser.g4 | 4 +- packages/malloy/src/lang/malloy-to-ast.ts | 24 +++------ packages/malloy/src/lang/parse-log.ts | 1 + .../malloy/src/lang/test/literals.spec.ts | 18 +++++++ .../malloy/src/lang/test/parse-expects.ts | 11 ++++ 7 files changed, 89 insertions(+), 27 deletions(-) diff --git a/packages/malloy/src/lang/ast/expressions/expr-array-literal.ts b/packages/malloy/src/lang/ast/expressions/expr-array-literal.ts index a7e9836a2..22cf649cd 100644 --- a/packages/malloy/src/lang/ast/expressions/expr-array-literal.ts +++ b/packages/malloy/src/lang/ast/expressions/expr-array-literal.ts @@ -10,6 +10,7 @@ import {ExprValue, computedExprValue} from '../types/expr-value'; import {ExpressionDef} from '../types/expression-def'; import {FieldSpace} from '../types/field-space'; import * as TDU from '../typedesc-utils'; +import {RecordLiteral} from './expr-record-literal'; export class ArrayLiteral extends ExpressionDef { elementType = 'array literal'; @@ -24,7 +25,10 @@ export class ArrayLiteral extends ExpressionDef { let firstValue: ExprValue | undefined = undefined; if (this.elements.length > 0) { for (const nextElement of this.elements) { - const v = nextElement.getExpression(fs); + const v = + firstValue && nextElement instanceof RecordLiteral + ? nextElement.getNextElement(fs, firstValue) + : nextElement.getExpression(fs); fromValues.push(v); if (v.type === 'error') { continue; diff --git a/packages/malloy/src/lang/ast/expressions/expr-record-literal.ts b/packages/malloy/src/lang/ast/expressions/expr-record-literal.ts index 5ea188410..480d0f197 100644 --- a/packages/malloy/src/lang/ast/expressions/expr-record-literal.ts +++ b/packages/malloy/src/lang/ast/expressions/expr-record-literal.ts @@ -11,15 +11,29 @@ import {ExpressionDef} from '../types/expression-def'; import {FieldSpace} from '../types/field-space'; import {MalloyElement} from '../types/malloy-element'; import * as TDU from '../typedesc-utils'; +import {ExprIdReference} from './expr-id-reference'; +export type ElementDetails = + | {path: ExprIdReference} + | {key?: string; value: ExpressionDef}; export class RecordElement extends MalloyElement { elementType = 'record element'; - constructor( - readonly key: string, - readonly value: ExpressionDef - ) { + value: ExpressionDef; + key?: string; + constructor(val: ElementDetails) { super(); - this.has({value}); + if ('value' in val) { + this.value = val.value; + this.has({value: val.value}); + if (val.key) { + this.key = val.key; + } + } else { + this.has({path: val.path}); + this.value = val.path; + const parts = val.path.fieldReference.path; + this.key = parts[parts.length - 1]; + } } } @@ -31,6 +45,10 @@ export class RecordLiteral extends ExpressionDef { } getExpression(fs: FieldSpace): ExprValue { + return this.getRecord(fs, []); + } + + getRecord(fs: FieldSpace, kidNames: string[]): ExprValue { const recLit: RecordLiteralNode = { node: 'recordLiteral', kids: {}, @@ -40,14 +58,24 @@ export class RecordLiteral extends ExpressionDef { }, }; const dependents: ExprValue[] = []; + let kidIndex = 0; for (const el of this.pairs) { + const key = el.key ?? kidNames[kidIndex]; + kidIndex += 1; + if (key === undefined) { + el.logError( + 'record-literal-needs-keys', + 'Anonymous record element not legal here' + ); + continue; + } const xVal = el.value.getExpression(fs); if (TD.isAtomic(xVal)) { dependents.push(xVal); - recLit.kids[el.key] = xVal.value; - recLit.typeDef.fields.push(mkFieldDef(TDU.atomicDef(xVal), el.key)); + recLit.kids[key] = xVal.value; + recLit.typeDef.fields.push(mkFieldDef(TDU.atomicDef(xVal), key)); } else { - this.logError( + el.value.logError( 'illegal-record-property-type', `Record property '${el.key} is type '${xVal.type}', which is not a legal property value type` ); @@ -59,4 +87,12 @@ export class RecordLiteral extends ExpressionDef { from: dependents, }); } + + getNextElement(fs: FieldSpace, headValue: ExprValue): ExprValue { + const recLit = headValue.value; + if (recLit.node === 'recordLiteral') { + return this.getRecord(fs, Object.keys(recLit.kids)); + } + return this.getRecord(fs, []); + } } diff --git a/packages/malloy/src/lang/grammar/MalloyParser.g4 b/packages/malloy/src/lang/grammar/MalloyParser.g4 index 2cb095f6a..c01d67d8a 100644 --- a/packages/malloy/src/lang/grammar/MalloyParser.g4 +++ b/packages/malloy/src/lang/grammar/MalloyParser.g4 @@ -610,8 +610,8 @@ caseWhen recordKey: id; recordElement - : fieldPath # recordRef - | recordKey IS fieldExpr # recordExpr + : fieldPath # recordRef + | (recordKey IS)? fieldExpr # recordExpr ; argumentList diff --git a/packages/malloy/src/lang/malloy-to-ast.ts b/packages/malloy/src/lang/malloy-to-ast.ts index 9c971d27d..82f4de326 100644 --- a/packages/malloy/src/lang/malloy-to-ast.ts +++ b/packages/malloy/src/lang/malloy-to-ast.ts @@ -1927,32 +1927,24 @@ export class MalloyToAST } visitRecordRef(pcx: parse.RecordRefContext) { - const pathCx = pcx.fieldPath(); - const tailEl = pathCx.fieldName().at(-1); - if (tailEl) { - const elementKey = getId(tailEl); - const idRef = new ast.ExprIdReference( - this.getFieldPath(pathCx, ast.ExpressionFieldReference) - ); - return new ast.RecordElement(elementKey, idRef); - } - throw this.internalError( - pathCx, - 'IMPOSSIBLY A PATH CONTAINED ZERO ELEMENTS' + const idRef = new ast.ExprIdReference( + this.getFieldPath(pcx.fieldPath(), ast.ExpressionFieldReference) ); + return this.astAt(new ast.RecordElement({path: idRef}), pcx); } visitRecordExpr(pcx: parse.RecordExprContext) { - const elementKey = getId(pcx.recordKey()); - const elementVal = this.getFieldExpr(pcx.fieldExpr()); - return new ast.RecordElement(elementKey, elementVal); + const value = this.getFieldExpr(pcx.fieldExpr()); + const keyCx = pcx.recordKey(); + const recInit = keyCx ? {key: getId(keyCx), value} : {value}; + return this.astAt(new ast.RecordElement(recInit), pcx); } visitExprLiteralRecord(pcx: parse.ExprLiteralRecordContext) { const els = this.only( pcx.recordElement().map(elCx => this.astAt(this.visit(elCx), elCx)), visited => visited instanceof ast.RecordElement && visited, - 'a key value pair' + 'a legal record property description' ); return new ast.RecordLiteral(els); } diff --git a/packages/malloy/src/lang/parse-log.ts b/packages/malloy/src/lang/parse-log.ts index cd8caff60..fc1d07785 100644 --- a/packages/malloy/src/lang/parse-log.ts +++ b/packages/malloy/src/lang/parse-log.ts @@ -367,6 +367,7 @@ type MessageParameterTypes = { 'sql-is-not-null': string; 'sql-is-null': string; 'illegal-record-property-type': string; + 'record-literal-needs-keys': string; 'not-yet-implemented': string; 'sql-case': string; 'case-then-type-does-not-match': { diff --git a/packages/malloy/src/lang/test/literals.spec.ts b/packages/malloy/src/lang/test/literals.spec.ts index 414d4230b..e0a1406eb 100644 --- a/packages/malloy/src/lang/test/literals.spec.ts +++ b/packages/malloy/src/lang/test/literals.spec.ts @@ -283,4 +283,22 @@ describe('literals', () => { }); test('a string containing a tab', () => expect(expr`'\t'`).toParse()); }); + describe('compound literals', () => { + test('simple record literal', () => { + expect('{answer is 42}').compilesTo('{answer:42}'); + }); + test('record literal with path', () => { + expect('{aninline.column}').compilesTo('{column:aninline.column}'); + }); + test('array of records with same schema', () => { + expect( + '[{name is "one", val is 1},{name is "two", val is 2}]' + ).compilesTo('[{name:"one", val:1}, {name:"two", val:2}]'); + }); + test('array of records with head schema', () => { + expect('[{name is "one", val is 1},{"two", 2}]').compilesTo( + '[{name:"one", val:1}, {name:"two", val:2}]' + ); + }); + }); }); diff --git a/packages/malloy/src/lang/test/parse-expects.ts b/packages/malloy/src/lang/test/parse-expects.ts index 5b49130f8..7aff60d07 100644 --- a/packages/malloy/src/lang/test/parse-expects.ts +++ b/packages/malloy/src/lang/test/parse-expects.ts @@ -229,6 +229,17 @@ function eToStr(e: Expr, symbols: ESymbols): string { return `"${e.literal}"`; case 'timeLiteral': return `@${e.literal}`; + case 'recordLiteral': { + const parts: string[] = []; + for (const [name, val] of Object.entries(e.kids)) { + parts.push(`${name}:${subExpr(val)}`); + } + return `{${parts.join(', ')}}`; + } + case 'arrayLiteral': { + const parts = e.kids.values.map(k => subExpr(k)); + return `[${parts.join(', ')}]`; + } case 'regexpLiteral': return `/${e.literal}/`; case 'trunc':