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

Allow literal record array to have implied keys #2048

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
52 changes: 44 additions & 8 deletions packages/malloy/src/lang/ast/expressions/expr-record-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
}

Expand All @@ -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: {},
Expand All @@ -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`
);
Expand All @@ -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, []);
}
}
4 changes: 2 additions & 2 deletions packages/malloy/src/lang/grammar/MalloyParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,8 @@ caseWhen

recordKey: id;
recordElement
: fieldPath # recordRef
| recordKey IS fieldExpr # recordExpr
: fieldPath # recordRef
| (recordKey IS)? fieldExpr # recordExpr
;

argumentList
Expand Down
24 changes: 8 additions & 16 deletions packages/malloy/src/lang/malloy-to-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ast.RecordElement>(
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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/malloy/src/lang/parse-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down
18 changes: 18 additions & 0 deletions packages/malloy/src/lang/test/literals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}]'
);
});
});
});
11 changes: 11 additions & 0 deletions packages/malloy/src/lang/test/parse-expects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
Loading