Skip to content

Commit

Permalink
Ast: refactor access-op as unary exprs
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanjermakov committed Mar 17, 2024
1 parent 4a1a5fc commit 342ce15
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 195 deletions.
80 changes: 50 additions & 30 deletions src/ast/ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,9 @@ describe('ast', () => {
expect(compactAstNode(ast.block)).toEqual(
{ kind: 'block',
statements:
[ { kind: 'binary-expr',
binaryOp: { kind: 'access-op' },
lOperand: { kind: 'identifier', names: [{ kind: 'name', value: 'a' }], typeArgs: [] },
rOperand: { kind: 'identifier', names: [{ kind: 'name', value: 'b' }], typeArgs: [] } } ] }
[ { kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
op: { kind: 'field-access-op', name: { kind: 'name', value: 'b' } } } ] }
)
})
})
Expand Down Expand Up @@ -137,13 +136,25 @@ describe('ast', () => {
expect(compactAstNode(ast.block)).toEqual(
{ kind: 'block',
statements:
[ { kind: 'binary-expr',
binaryOp: { kind: 'access-op' },
lOperand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
rOperand:
{ kind: 'unary-expr',
op: { kind: 'call-op', args: [] },
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'b' } ], typeArgs: [] } } } ] }
[ { kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
op: { kind: 'method-call-op', name: { kind: 'name', value: 'b' }, typeArgs: [], call: { kind: 'call-op', args: [] } } } ] }
)
})

it('method call with type args', () => {
const ast = buildAst('a.b<A>()')
// biome-ignore format: compact
expect(compactAstNode(ast.block)).toEqual(
{ kind: 'block',
statements:
[ { kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
op:
{ kind: 'method-call-op',
name: { kind: 'name', value: 'b' },
typeArgs: [ { kind: 'identifier', names: [ { kind: 'name', value: 'A' } ], typeArgs: [] } ],
call: { kind: 'call-op', args: [] } } } ] }
)
})

Expand All @@ -153,27 +164,36 @@ describe('ast', () => {
expect(compactAstNode(ast.block)).toEqual(
{ kind: 'block',
statements:
[ { kind: 'binary-expr',
binaryOp: { kind: 'access-op' },
lOperand:
{ kind: 'binary-expr',
binaryOp: { kind: 'access-op' },
lOperand:
{ kind: 'binary-expr',
binaryOp: { kind: 'access-op' },
lOperand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
rOperand:
{ kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'b' } ], typeArgs: [] },
op: { kind: 'call-op', args: [] } } },
rOperand:
[ { kind: 'unary-expr',
operand:
{ kind: 'unary-expr',
operand:
{ kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'c' } ], typeArgs: [] },
op: { kind: 'call-op', args: [] } } },
rOperand:
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
op: { kind: 'method-call-op', name: { kind: 'name', value: 'b' }, typeArgs: [], call: { kind: 'call-op', args: [] } } },
op: { kind: 'method-call-op', name: { kind: 'name', value: 'c' }, typeArgs: [], call: { kind: 'call-op', args: [] } } },
op: { kind: 'method-call-op', name: { kind: 'name', value: 'd' }, typeArgs: [], call: { kind: 'call-op', args: [] } } } ] }
)
})

it('postfix op chain', () => {
const ast = buildAst('a.b()!?()')
// biome-ignore format: compact
expect(compactAstNode(ast.block)).toEqual(
{ kind: 'block',
statements:
[ { kind: 'unary-expr',
operand:
{ kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'd' } ], typeArgs: [] },
op: { kind: 'call-op', args: [] } } } ] }
operand:
{ kind: 'unary-expr',
operand:
{ kind: 'unary-expr',
operand: { kind: 'identifier', names: [ { kind: 'name', value: 'a' } ], typeArgs: [] },
op: { kind: 'method-call-op', name: { kind: 'name', value: 'b' }, typeArgs: [], call: { kind: 'call-op', args: [] } } },
op: { kind: 'unwrap-op' } },
op: { kind: 'bind-op' } },
op: { kind: 'call-op', args: [] } } ] }
)
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const astInfixOpKinds = <const>[
'assign-op'
]

export const astPostfixOpKinds = <const>['call-op', 'unwrap-op', 'bind-op']
export const astPostfixOpKinds = <const>['method-call-op', 'field-access-op', 'call-op', 'unwrap-op', 'bind-op']

export const astKinds = <const>[
'module',
Expand Down
56 changes: 46 additions & 10 deletions src/ast/op.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,35 @@ import { MethodDef, VariantDef } from '../scope/vid'
import { Static } from '../semantic'
import { ConcreteGeneric } from '../typecheck'
import { Arg, AstNode, AstNodeKind, buildArg } from './index'
import { Name, buildName } from './operand'
import { Type, buildType } from './type'

export type PostfixOp = CallOp | UnwrapOp | BindOp
export type PostfixOp = MethodCallOp | FieldAccessOp | CallOp | UnwrapOp | BindOp

export const isPostfixOp = (op: AstNode<AstNodeKind>): op is PostfixOp => {
return op.kind === 'call-op' || op.kind === 'unwrap-op' || op.kind === 'bind-op'
return (
op.kind === 'method-call-op' ||
op.kind === 'field-access-op' ||
op.kind === 'call-op' ||
op.kind === 'unwrap-op' ||
op.kind === 'bind-op'
)
}

export const buildPostfixOp = (node: ParseNode): PostfixOp => {
if (node.kind === 'call-op') {
return buildCallOp(node)
switch (node.kind) {
case 'method-call-op':
return buildMethodCallOp(node)
case 'field-access-op':
return buildFieldAccessOp(node)
case 'call-op':
return buildCallOp(node)
case 'unwrap-op':
case 'bind-op':
return { kind: node.kind, parseNode: node }
default:
throw Error(`expected postfix-op, got ${node.kind}`)
}
if (node.kind === 'unwrap-op' || node.kind === 'bind-op') {
return { kind: node.kind, parseNode: node }
}
throw Error(`expected prefix-op, got ${node.kind}`)
}

export type BinaryOp = (
Expand Down Expand Up @@ -49,7 +63,6 @@ export const associativityMap: Map<AstNodeKind, Associativity> = new Map([
['div-op', 'left'],
['exp-op', 'right'],
['mod-op', 'left'],
['access-op', 'left'],
['eq-op', 'none'],
['ne-op', 'none'],
['ge-op', 'none'],
Expand All @@ -72,7 +85,6 @@ export const precedenceMap: Map<AstNodeKind, number> = new Map([
['div-op', 12],
['exp-op', 13],
['mod-op', 12],
['access-op', 17],
['eq-op', 8],
['ne-op', 8],
['ge-op', 9],
Expand Down Expand Up @@ -110,6 +122,30 @@ export const buildBinaryOp = (node: ParseNode): BinaryOp => {
return { kind: <any>node.kind, parseNode: node }
}

export interface MethodCallOp extends AstNode<'method-call-op'> {
name: Name
typeArgs: Type[]
call: CallOp
}

export const buildMethodCallOp = (node: ParseNode): MethodCallOp => {
const nodes = filterNonAstNodes(node)
let i = 0
const name = buildName(nodes[i++])
const typeArgs = nodes[i].kind === 'type-args' ? filterNonAstNodes(nodes[i++]).map(buildType) : []
const call = buildCallOp(nodes[i++])
return { kind: 'method-call-op', parseNode: node, name, typeArgs, call }
}

export interface FieldAccessOp extends AstNode<'field-access-op'> {
name: Name
}

export const buildFieldAccessOp = (node: ParseNode): FieldAccessOp => {
const name = buildName(filterNonAstNodes(node)[0])
return { kind: 'field-access-op', parseNode: node, name }
}

export interface CallOp extends AstNode<'call-op'>, Partial<Static> {
args: Arg[]
methodDef?: MethodDef
Expand Down
61 changes: 27 additions & 34 deletions src/codegen/js/expr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ export const emitOperandExpr = (operandExpr: OperandExpr, module: Module, ctx: C
export const emitUnaryExpr = (unaryExpr: UnaryExpr, module: Module, ctx: Context): EmitExpr => {
const resultVar = nextVariable(ctx)
switch (unaryExpr.op.kind) {
case 'method-call-op': {
const lOp = emitOperand(unaryExpr.operand, module, ctx)
const mCall = unaryExpr.op
const call = mCall.call
const methodDef = call.methodDef!
const methodName = methodDef.fn.name.value
const args = call.args.map(a => emitExpr(a.expr, module, ctx))
const genericTypes = call.generics?.map(g => emitGeneric(g, module, ctx)) ?? []
const jsArgs = [...args, ...genericTypes]
const argsEmit = (
methodDef.fn.static ? jsArgs.map(a => a.resultVar) : [lOp.resultVar, ...jsArgs.map(a => a.resultVar)]
).join(', ')
const upcasts = unaryExpr.operand.upcasts
const upcastEmit = upcasts ? emitUpcasts(lOp.resultVar, upcasts) : ''
const callerEmit = call.impl ? jsRelName(call.impl) : `${lOp.resultVar}.${relTypeName(methodDef.rel)}`
const callEmit = jsVariable(resultVar, `${callerEmit}().${methodName}(${argsEmit})`)
return {
emit: emitLines([lOp.emit, upcastEmit, emitLines(jsArgs.map(a => a.emit)), callEmit]),
resultVar
}
}
case 'field-access-op':
const lOp = emitOperand(unaryExpr.operand, module, ctx)
return {
emit: emitLines([lOp.emit, jsVariable(resultVar, `${lOp.resultVar}.value.${unaryExpr.op.name.value}`)]),
resultVar
}
case 'call-op':
const call = unaryExpr.op
const args = call.args.map(a => {
Expand Down Expand Up @@ -75,40 +102,6 @@ export const emitBinaryExpr = (binaryExpr: BinaryExpr, module: Module, ctx: Cont
const resultVar = nextVariable(ctx)
const rOp = emitOperand(binaryExpr.rOperand, module, ctx)
switch (binaryExpr.binaryOp.kind) {
case 'access-op': {
if (binaryExpr.rOperand.kind === 'identifier') {
const accessor = binaryExpr.rOperand.names.at(-1)!.value
return {
emit: emitLines([lOp.emit, jsVariable(resultVar, `${lOp.resultVar}.value.${accessor}`)]),
resultVar
}
}
if (binaryExpr.rOperand.kind === 'unary-expr' && binaryExpr.rOperand.op.kind === 'call-op') {
const call = binaryExpr.rOperand.op
const methodDef = call.methodDef!
const methodName = methodDef.fn.name.value
const args = call.args.map(a => emitExpr(a.expr, module, ctx))
const genericTypes = call.generics?.map(g => emitGeneric(g, module, ctx)) ?? []
const jsArgs = [...args, ...genericTypes]
const argsEmit = (
methodDef.fn.static
? jsArgs.map(a => a.resultVar)
: [lOp.resultVar, ...jsArgs.map(a => a.resultVar)]
).join(', ')
const upcasts = binaryExpr.lOperand.upcasts
const upcastEmit = upcasts ? emitUpcasts(lOp.resultVar, upcasts) : ''
const callerEmit = call.impl ? jsRelName(call.impl) : `${lOp.resultVar}.${relTypeName(methodDef.rel)}`
const callEmit = jsVariable(resultVar, `${callerEmit}().${methodName}(${argsEmit})`)
return {
emit: emitLines([lOp.emit, upcastEmit, emitLines(jsArgs.map(a => a.emit)), callEmit]),
resultVar
}
}
return {
emit: jsError('unwrap/bind ops'),
resultVar
}
}
case 'assign-op': {
return {
emit: emitLines([
Expand Down
7 changes: 6 additions & 1 deletion src/parser/fns/expr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export const parseExpr = (parser: Parser): void => {
export const parseSubExpr = (parser: Parser): void => {
const mark = parser.open()
parseOperand(parser)
while (parser.at('o-paren') || (parser.at('excl') && parser.nth(1) !== 'equals') || parser.at('qmark')) {
while (
parser.at('period') ||
parser.at('o-paren') ||
(parser.at('excl') && parser.nth(1) !== 'equals') ||
parser.at('qmark')
) {
parsePostfixOp(parser)
}
parser.close(mark, 'sub-expr')
Expand Down
2 changes: 0 additions & 2 deletions src/parser/fns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { parseTypeAnnot } from './type'
*/
export const nameLikeTokens: TokenKind[] = ['name', ...lexerKeywordKinds]

export const postfixOpFirstTokens: TokenKind[] = ['o-paren', 'excl', 'qmark']
export const infixOpFirstTokens: TokenKind[] = [
'ampersand',
'asterisk',
Expand All @@ -21,7 +20,6 @@ export const infixOpFirstTokens: TokenKind[] = [
'minus',
'o-angle',
'percent',
'period',
'pipe',
'plus',
'slash'
Expand Down
44 changes: 38 additions & 6 deletions src/parser/fns/op.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { nameLikeTokens } from '.'
import { Parser } from '..'
import { syntaxError } from '../../error'
import { parseExpr } from './expr'
import { parseExpr, parseTypeArgs } from './expr'

/**
* infix-op ::= add-op | sub-op | mult-op | div-op | exp-op | mod-op | access-op | eq-op | ne-op | ge-op | le-op | gt-op
* infix-op ::= add-op | sub-op | mult-op | div-op | exp-op | mod-op | eq-op | ne-op | ge-op | le-op | gt-op
* | lt-op | and-op | or-op | assign-op;
*/
export const parseInfixOp = (parser: Parser): void => {
Expand Down Expand Up @@ -33,10 +33,6 @@ export const parseInfixOp = (parser: Parser): void => {
parser.close(mark, 'mod-op')
return
}
if (parser.consume('period')) {
parser.close(mark, 'access-op')
return
}
if (parser.at('equals') && parser.nth(1) === 'equals') {
parser.advance()
parser.advance()
Expand Down Expand Up @@ -91,6 +87,18 @@ export const parsePostfixOp = (parser: Parser): void => {
parseCallOp(parser)
return
}
if (parser.at('period')) {
if (
parser.nth(2) === 'o-paren' ||
(parser.nth(2) === 'o-angle' &&
parser.encounter('c-angle', [...nameLikeTokens, 'comma', 'o-angle', 'underscore'], 2))
) {
parseMethodCallOp(parser)
} else {
parseFieldAccessOp(parser)
}
return
}
const mark = parser.open()
if (parser.consume('excl')) {
parser.close(mark, 'unwrap-op')
Expand All @@ -103,6 +111,30 @@ export const parsePostfixOp = (parser: Parser): void => {
parser.advanceWithError(syntaxError(parser, 'expected postfix operator'), mark)
}

/*
* method-call-op ::= PERIOD NAME type-args? call-op
*/
export const parseMethodCallOp = (parser: Parser): void => {
const mark = parser.open()
parser.expect('period')
parser.expectAny(nameLikeTokens)
if (parser.at('o-angle')) {
parseTypeArgs(parser)
}
parseCallOp(parser)
parser.close(mark, 'method-call-op')
}

/*
* field-access-op ::= PERIOD NAME
*/
export const parseFieldAccessOp = (parser: Parser): void => {
const mark = parser.open()
parser.expect('period')
parser.expectAny(nameLikeTokens)
parser.close(mark, 'field-access-op')
}

/**
* call-op ::= O-PAREN (arg (COMMA arg)*)? COMMA? C-PAREN
*/
Expand Down
Loading

0 comments on commit 342ce15

Please sign in to comment.