Metamorphing Machine I rather be this walking metamorphosis
than having that old formed opinion about everything!

Let's build a transpiler! Part 28

This is the twenty-eighth post in a series of building a transpiler.
You can find the previous ones here.

Last time I said we'll deal with Dims, Consts, Static variables, line numbers, and labels.
The first three are somewhat easier, as we already have ParseDim and ParseConsts procedures.
We have to do some minor changes to ParseDim, though:

Private Sub ParseDim( _
ByVal Access As Accessibility, _
ByVal Entity As Entity, _
ByVal Vars As KeyedList, _
Optional ByVal InsideProc As Boolean, _
Optional ByVal IsStatic As Boolean, _
Optional ByVal Token As Token _
)
Dim Name As String
Dim WasArray As Boolean
Dim Tkn As Token
Dim Lit As Literal
Dim Var As Variable
Dim Expr As IExpression
Dim Subs As SubscriptPair
Dim Xp As Expressionist
Dim Uni As UnaryExpression
Dim Bin As BinaryExpression

If InsideProc Then: If Access = acPublic Or Access = acPrivate Then Fail Token, Msg063
If Token Is Nothing Then Set Token = NextToken

Set Xp = New Expressionist
Xp.CanHaveTo = True

Do
Set Var = New Variable
Var.Access = Access
Var.IsStatic = IsStatic

If Token.IsKeyword(kwWithEvents) Then
If Not Entity.IsClass Then Fail Token, Msg016
If InsideProc Then Fail Token, Msg063

Var.HasWithEvents = True
Set Token = NextToken
End If

If Not IsProperId(Token, CanHaveSuffix:=True) Then Fail Token, Msg061, Msg003
Set Var.Id.Name = Token

Set Token = NextToken
WasArray = False

If Token.Kind = tkLeftParenthesis Then

Do
Set Expr = Xp.GetExpression(Me)
Set Token = Xp.LastToken

If Not Expr Is Nothing Then
Select Case Expr.Kind
Case ekLiteral, ekSymbol, ekUnaryExpr
Set Subs = New SubscriptPair
Set Subs.LowerBound = SynthLower(Entity)
Set Subs.UpperBound = Expr

Case ekBinaryExpr
Set Bin = Expr
Set Subs = New SubscriptPair

If Bin.Operator.Value.IsOperator(opTo) Then
Set Subs.LowerBound = Bin.LHS
Set Subs.UpperBound = Bin.RHS
Else
Set Subs.LowerBound = SynthLower(Entity)
Set Subs.UpperBound = Expr
End If

Case Else
Fail Token, Msg065
End Select

Var.Subscripts.Add Subs
End If

If Token.Kind <> tkListSeparator Then Exit Do
Loop

If Token.Kind <> tkRightParenthesis And Xp.LastToken.Kind <> tkRightParenthesis Then Fail Token, Msg057

WasArray = True
Set Token = NextToken
End If

If Token.IsKeyword(kwAs) Then
If Var.Id.Name.Suffix <> vbNullChar Then Fail Token, Msg024
Set Token = NextToken

If Token.IsOperator(opNew) Then
Var.HasNew = True
Set Token = NextToken
End If

If Not IsProperDataType(Token) Then Fail Token, Msg061, Msg025
Set Var.DataType = NewDataType(Token)

If Var.HasNew And Var.DataType.Id.Name.Kind = tkKeyword Then Fail Token, Msg062, Msg059

Set Token = NextToken

If Token.IsOperator(opDot) Then
Set Token = NextToken

If Not IsProperDataType(Token) Then Fail Token, Msg061, Msg003
Set Var.DataType.Id.Name = Token

Set Token = NextToken
End If

ElseIf Var.Id.Name.Suffix <> vbNullChar Then
Set Var.DataType = FromChar(Var.Id.Name.Suffix)

Else
Set Var.DataType = Entity.DefTypes(NameOf(Var.Id.Name))
End If

If Token.IsOperator(opMul) Then
Set Var.DataType.FixedLength = Xp.GetExpression(Me)
Set Token = Xp.LastToken
End If

Var.DataType.IsArray = WasArray
If Var.HasNew And Var.DataType.IsArray Then Fail Token, Msg064

If Token.IsOperator(opEq) Then
Set Var.Init = Xp.GetExpression(Me)
Set Token = Xp.LastToken
End If

Name = NameOf(Var.Id.Name)
If Not InsideProc Then CheckDupl Entity, Var.Id.Name
If Vars.Exists(Name) Then Fail Token, Msg006 & Name
Vars.Add Var, Name

If IsBreak(Token) Then Exit Do
If Token.Kind <> tkListSeparator Then Fail Token, Msg061, ","
Set Token = NextToken
Loop
End Sub

Previously we were adding the parsed variables directly to Entity's Vars property, but now we may need to add them somewhere else, so this somewhere else is being provided through the Vars parameter.
Also, we were adding Static to the variable outside ParseDim, now we are passing a flag to it so it can set every parsed variable properly.
Finally, we were checking if there was another variable with the same name in the declaration area, raising an error if there was one.
Now we cannot do that because we're in a different scope (we're inside a Sub, Function, or Property, and thus allowed to have a variable with the same name as some other in a higher scope.)
So ParseDim got an InsideProcedure parameter as ParseConsts already had. We only check for duplicates in the declaration area if we're not inside a procedure.

We have to repeat the same to ParseConsts, too:

(...)
If Not InsideProc Then CheckDupl Entity, Cnt.Id.Name
Name = NameOf(Cnt.Id.Name)
If Body.Exists(Name) Then Fail Cnt.Id.Name, Msg006 & Name

If Cnt.DataType Is Nothing Then
Rem TODO: Infer its data type
End If

Rem Save it
Body.AddKeyValue NameOf(Cnt.Id.Name), Cnt

Rem Move on
Set Token = Xp.LastToken

If IsBreak(Token) Then Exit Do
If InsideProc And Token.IsKeyword(kwElse) Then Exit Do

If Token.Kind <> tkListSeparator Then Fail Token, Msg023, Msg027
(...)

We also have to provide another way out of the loop if the Const is being declared in an If statement.
It is silly but allowed to have something like this:

If SomeCondition Then Const Abc = 1

The constant is declared regardless of the condition, though. As I said, silly, but legal.

With that in place, we'll start coding the ParseBody sub:

Private Sub ParseBody(ByVal Entity As Entity, ByVal Body As KeyedList, ByVal ClosingToken As Long)
Dim Token As Token
Dim LookAhead As Token

Do
If LookAhead Is Nothing Then
Set Token = SkipLineBreaks
Else
Set Token = LookAhead
Set LookAhead = Nothing
End If

Select Case Token.Kind
Case tkKeyword
Select Case Token.Code
Case kwEnd
Rem Is it the End statement of End Sub, Function, or Property?
Set LookAhead = NextToken
If LookAhead.IsKeyword(ClosingToken) Then Exit Do
If LookAhead.Kind = tkIdentifier And LookAhead.Code = cxProperty Then Exit Do
Body.Add New EndConstruct

Case kwDim
ParseDim acLocal, Entity, Body, InsideProc:=True

Case kwStatic
ParseDim acLocal, Entity, Body, InsideProc:=True, IsStatic:=True

Case kwConst
ParseConsts acLocal, Entity, Body, InsideProc:=True

Case Else
Stop
End Select

Case tkEndOfStream
Exit Do

Case Else
Stop
End Select
Loop
End Sub

A few things to note:
Moving on, let's handle line numbers. ParseBody will work on several lines one at a time, so when the first thing we get is an integer token we can safely assume it is a line number:

Private Sub ParseBody(ByVal Entity As Entity, ByVal Body As KeyedList, ByVal ClosingToken As Long)
Dim Token As Token
Dim LookAhead As Token
Dim LinNum As LineNumberConstruct

Do
If LookAhead Is Nothing Then
Set Token = SkipLineBreaks
Else
Set Token = LookAhead
Set LookAhead = Nothing
End If

Rem Do we have a line number?
If Token.Kind = tkIntegerNumber Then
Set LinNum = New LineNumberConstruct
Set LinNum.Value = Token
Body.Add LinNum
Set Token = NextToken
End If

(...)
End Sub


Public Class LineNumberConstruct
Option Explicit
Implements IStmt

Public Value As Token

Private Property Get IStmt_Kind() As StmtNumbers
IStmt_Kind = snLineNumber
End Property
End Class

Next, we'll parse labels. If we get an identifier, we check if the next token is a colon. If it is, then we have a label:

Private Sub ParseBody(ByVal Entity As Entity, ByVal Body As KeyedList, ByVal ClosingToken As Long)
Dim Token As Token
Dim LookAhead As Token
Dim LinNum As LineNumberConstruct
Dim Label As LabelConstruct

Do
If LookAhead Is Nothing Then
Set Token = SkipLineBreaks
Else
Set Token = LookAhead
Set LookAhead = Nothing
End If

Rem Do we have a line number?
If Token.Kind = tkIntegerNumber Then
Set LinNum = New LineNumberConstruct
Set LinNum.Value = Token
Body.Add LinNum
Set Token = NextToken
End If

Rem Do we have a label?
If Token.Kind = tkIdentifier Then
Set LookAhead = NextToken

If LookAhead.Kind = tkSoftLineBreak Then
Set Label = New LabelConstruct
Set Label.Id = NewId(Token)
Body.Add Label
Set LookAhead = Nothing
Set Token = NextToken
End If
End If

(...)
End Sub


Public Class LabelConstruct
Option Explicit
Implements IStmt

Public Id As Identifier

Private Property Get IStmt_Kind() As StmtNumbers
IStmt_Kind = snLabel
End Property
End Class

As strange as it may be, VB allows a line to start with a line number followed by a label and then by a statement, like "120 Here: Stop".

Next week we'll parse the If statement. You'll see that it is not that easy as it seems to be.

Andrej Biasic
2021-03-10