Skip to content

Commit

Permalink
Implement 'maybe' and 'safe' statement features with associated parsi…
Browse files Browse the repository at this point in the history
…ng and type checking
  • Loading branch information
itsfuad committed Dec 10, 2024
1 parent f645c10 commit a9e68a7
Show file tree
Hide file tree
Showing 24 changed files with 877 additions and 101 deletions.
4 changes: 1 addition & 3 deletions analyzer/arrays.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,11 @@ func evaluateIndexableAccess(indexable ast.Indexable, e *TypeEnvironment) TcValu
return NewInt(8, false)
case Map:
//if key is interface then error
if t.KeyType.DType() == INTERFACE_TYPE {
//return t.ValueType, fmt.Errorf("cannot access index of type %s", INTERFACE_TYPE)
if unwrapType(t.KeyType).DType() == INTERFACE_TYPE {
errgen.AddError(e.filePath, indexable.Start.Line, indexable.End.Line, indexable.Index.StartPos().Column, indexable.Index.EndPos().Column, fmt.Sprintf("cannot access index of type %s", INTERFACE_TYPE)).ErrorLevel(errgen.NORMAL)
}
indexedValueType = t.ValueType
default:
//return nil, fmt.Errorf("cannot access index of type %s", container.DType())
errgen.AddError(e.filePath, indexable.Start.Line, indexable.End.Line, indexable.Container.StartPos().Column, indexable.Container.EndPos().Column, fmt.Sprintf("cannot access index of type %s", container.DType())).ErrorLevel(errgen.CRITICAL)
}

Expand Down
1 change: 0 additions & 1 deletion analyzer/conditionals.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ func checkIfStmt(ifNode ast.IfStmt, env *TypeEnvironment) TcValue {
//condition
cond := CheckAST(ifNode.Condition, env)
if cond.DType() != BOOLEAN_TYPE {

errgen.AddError(env.filePath, ifNode.Condition.StartPos().Line, ifNode.Condition.EndPos().Line, ifNode.Condition.StartPos().Column, ifNode.Condition.EndPos().Column, "Condition must be a boolean expression").ErrorLevel(errgen.NORMAL)
}

Expand Down
18 changes: 15 additions & 3 deletions analyzer/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const (
GLOBAL_SCOPE SCOPE_TYPE = iota
FUNCTION_SCOPE
STRUCT_SCOPE
SAFE_SCOPE
OTHERWISE_SCOPE
CONDITIONAL_SCOPE
LOOP_SCOPE
)
Expand Down Expand Up @@ -64,7 +66,7 @@ func initVar(env *TypeEnvironment, name string, typeVar TcValue, isConst bool, i
}

builtinValues[name] = true

utils.BRIGHT_BROWN.Printf("Initialized builtin value '%s'\n", name)
}

Expand All @@ -81,14 +83,24 @@ func NewTypeENV(parent *TypeEnvironment, scope SCOPE_TYPE, scopeName string, fil
}
}

func (t *TypeEnvironment) IsInStructScope() bool {
func (t *TypeEnvironment) isInFunctionScope() bool {
if t.scopeType == FUNCTION_SCOPE {
return true
}
if t.parent == nil {
return false
}
return t.parent.isInFunctionScope()
}

func (t *TypeEnvironment) isInStructScope() bool {
if t.scopeType == STRUCT_SCOPE {
return true
}
if t.parent == nil {
return false
}
return t.parent.IsInStructScope()
return t.parent.isInStructScope()
}

func (t *TypeEnvironment) resolveFunctionEnv() (*TypeEnvironment, error) {
Expand Down
19 changes: 9 additions & 10 deletions analyzer/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ func checkUnaryExpr(node ast.UnaryExpr, env *TypeEnvironment) TcValue {
errgen.AddError(env.filePath, op.Start.Line, op.End.Line, op.Start.Column, op.End.Column, "invalid unary operation with boolean types").ErrorLevel(errgen.NORMAL)
}
default:

errgen.AddError(env.filePath, op.Start.Line, op.End.Line, op.Start.Column, op.End.Column, fmt.Sprintf("this unary operation is not supported with %s types", t.DType())).ErrorLevel(errgen.NORMAL)
}

Expand All @@ -84,8 +83,8 @@ func checkBinaryExpr(node ast.BinaryExpr, env *TypeEnvironment) TcValue {
left := CheckAST(node.Left, env)
right := CheckAST(node.Right, env)

leftType := left.DType()
rightType := right.DType()
leftType := tcValueToString(left)
rightType := tcValueToString(right)

var errLineStart, errLineEnd, errStart, errEnd int
var errMsg string
Expand All @@ -96,13 +95,13 @@ func checkBinaryExpr(node ast.BinaryExpr, env *TypeEnvironment) TcValue {
case lexer.MINUS_TOKEN, lexer.MUL_TOKEN, lexer.DIV_TOKEN, lexer.MOD_TOKEN, lexer.EXP_TOKEN:
//must have to be numeric type on both side
if leftType != builtins.INT32 && leftType != builtins.FLOAT32 {
errMsg = "cannot perform numeric operation. left hand side expression must be evaluated to a numeric type"
errMsg = fmt.Sprintf("cannot perform numeric operation between type '%s' and '%s'. left hand side expression must be evaluated to a numeric type.", leftType, rightType)
errLineStart = node.Left.StartPos().Line
errLineEnd = node.Left.EndPos().Line
errStart = node.Left.StartPos().Column
errEnd = node.Left.EndPos().Column
} else if rightType != builtins.INT32 && rightType != builtins.FLOAT32 {
errMsg = "cannot perform numeric operation. right hand side expression must be evaluated to a numeric type"
errMsg = fmt.Sprintf("cannot perform numeric operation between type '%s' and '%s'. right hand side expression must be evaluated to a numeric type.", leftType, rightType)
errLineStart = node.Right.StartPos().Line
errLineEnd = node.Right.EndPos().Line
errStart = node.Right.StartPos().Column
Expand All @@ -126,8 +125,8 @@ func checkBinaryExpr(node ast.BinaryExpr, env *TypeEnvironment) TcValue {

func checkComparison(node ast.BinaryExpr, left TcValue, right TcValue, env *TypeEnvironment) TcValue {

leftType := left.DType()
rightType := right.DType()
leftType := tcValueToString(left)
rightType := tcValueToString(right)

op := node.Operator

Expand All @@ -154,16 +153,16 @@ func checkComparison(node ast.BinaryExpr, left TcValue, right TcValue, env *Type

func checkAdditionAndConcat(node ast.BinaryExpr, left TcValue, right TcValue, env *TypeEnvironment) TcValue {

leftType := left.DType()
rightType := right.DType()
leftType := tcValueToString(left)
rightType := tcValueToString(right)

var errLineStart, errLineEnd, errStart, errEnd int
var errMsg string
//only string concat, int and floats are allowed.
if leftType == builtins.INT32 || leftType == builtins.FLOAT32 {
//right has to be int or float
if rightType != builtins.INT32 && rightType != builtins.FLOAT32 {
errMsg = "cannot perform numeric operation. right hand side expression must be evaluated to a numeric type"
errMsg = fmt.Sprintf("cannot perform numeric operation between type '%s' and '%s'. right hand side expression must be evaluated to a numeric type.", leftType, rightType)
errLineStart = node.Right.StartPos().Line
errLineEnd = node.Right.EndPos().Line
errStart = node.Right.StartPos().Column
Expand Down
4 changes: 2 additions & 2 deletions analyzer/return.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

func checkReturnStmt(returnNode ast.ReturnStmt, env *TypeEnvironment) TcValue {
//check if the function is declared
if env.scopeType != FUNCTION_SCOPE {
errgen.AddError(env.filePath, returnNode.StartPos().Line, returnNode.EndPos().Line, returnNode.StartPos().Column, returnNode.EndPos().Column, "Return statement must be inside a function").ErrorLevel(errgen.CRITICAL)
if !env.isInFunctionScope() {
errgen.AddError(env.filePath, returnNode.StartPos().Line, returnNode.EndPos().Line, returnNode.StartPos().Column, returnNode.EndPos().Column, "return statement outside function").ErrorLevel(errgen.NORMAL)
}

//check if the return type matches the function return type
Expand Down
79 changes: 79 additions & 0 deletions analyzer/safe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package analyzer

import (
"fmt"
"walrus/ast"
"walrus/errgen"
)

// checkSafeStmt checks the safety of a given SafeStmt node within the provided type environment.
// It ensures that the value of the node is of 'maybe' type and validates the safe and unsafe blocks.
//
// Parameters:
// - node: The SafeStmt node to be checked.
// - env: The type environment in which the node is being checked.
//
// Returns:
// - TcValue: A type-checked value indicating the result of the safety check.
//
// Errors:
// - Adds a critical error if the node's value is not of 'maybe' type.
// - Adds normal errors if there are issues within the safe or unsafe blocks.
func checkSafeStmt(node ast.SafeStmt, env *TypeEnvironment) TcValue {

maybeVar := CheckAST(node.Value, env)

if maybeVar.DType() != MAYBE_TYPE {
errgen.AddError(env.filePath, node.Value.Start.Line, node.Value.End.Line, node.Value.Start.Column, node.Value.End.Column, "safe-otherwise can only be used with 'maybe' types").ErrorLevel(errgen.CRITICAL)
}

// check the safe block where the maybe type is the type of the defined type
err := checkSafeBlock(env, node.Value.Name, node.SafeBlock, maybeVar.(Maybe))
if err != nil {
errgen.AddError(env.filePath, node.SafeBlock.StartPos().Line, node.SafeBlock.EndPos().Line, node.SafeBlock.StartPos().Column, node.SafeBlock.EndPos().Column, err.Error()).ErrorLevel(errgen.NORMAL)
}
err = checkOtherwiseBlock(env, node.Value.Name, node.UnsafeBlock)
if err != nil {
errgen.AddError(env.filePath, node.UnsafeBlock.StartPos().Line, node.UnsafeBlock.EndPos().Line, node.UnsafeBlock.StartPos().Column, node.UnsafeBlock.EndPos().Column, err.Error()).ErrorLevel(errgen.NORMAL)
}

return NewVoid()
}

func checkSafeBlock(env *TypeEnvironment, name string, block ast.BlockStmt, value Maybe) error {

//new scope for the safe block
safeScope := NewTypeENV(env, SAFE_SCOPE, "safe block", env.filePath)

//declare the variable in the safe block
err := safeScope.declareVar(name, value.MaybeType, false, false)
if err != nil {
return fmt.Errorf("error declaring variable '%s' in safe block. "+err.Error(), name)
}

//check the block
for _, stmt := range block.Contents {
CheckAST(stmt, safeScope)
}

return nil
}

func checkOtherwiseBlock(env *TypeEnvironment, name string, block ast.BlockStmt) error {

//new scope for the unsafe block
unsafeScope := NewTypeENV(env, OTHERWISE_SCOPE, "unsafe block", env.filePath)

//declare the variable in the unsafe block
err := unsafeScope.declareVar(name, NewNull(), false, false)
if err != nil {
return fmt.Errorf("error declaring variable '%s' in unsafe block. "+err.Error(), name)
}

//check the block
for _, stmt := range block.Contents {
CheckAST(stmt, unsafeScope)
}

return nil
}
2 changes: 1 addition & 1 deletion analyzer/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func checkPropertyAccess(expr ast.StructPropertyAccessExpr, env *TypeEnvironment

if isPrivate {
//check the scope we are in
if !env.IsInStructScope() {
if !env.isInStructScope() {
errgen.AddError(env.filePath, prop.Start.Line, prop.End.Line, prop.Start.Column, prop.End.Column, fmt.Sprintf("cannot access private property '%s' from outside of the struct's scope", prop.Name)).ErrorLevel(errgen.NORMAL)
}
}
Expand Down
2 changes: 2 additions & 0 deletions analyzer/typecheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func CheckAST(node ast.Node, env *TypeEnvironment) TcValue {
return checkForStmt(t, env)
case ast.ReturnStmt:
return checkReturnStmt(t, env)
case ast.SafeStmt:
return checkSafeStmt(t, env)
case nil:
return NewNull()
}
Expand Down
10 changes: 10 additions & 0 deletions analyzer/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
INTERFACE_TYPE builtins.TC_TYPE = builtins.INTERFACE
ARRAY_TYPE builtins.TC_TYPE = builtins.ARRAY
MAP_TYPE builtins.TC_TYPE = builtins.MAP
MAYBE_TYPE builtins.TC_TYPE = builtins.MAYBE
USER_DEFINED_TYPE builtins.TC_TYPE = builtins.USER_DEFINED
BLOCK_TYPE builtins.TC_TYPE = "block"
RETURN_TYPE builtins.TC_TYPE = "return"
Expand Down Expand Up @@ -204,3 +205,12 @@ type Interface struct {
func (t Interface) DType() builtins.TC_TYPE {
return t.DataType
}

type Maybe struct {
DataType builtins.TC_TYPE
MaybeType TcValue
}

func (t Maybe) DType() builtins.TC_TYPE {
return t.DataType
}
4 changes: 4 additions & 0 deletions analyzer/typesMaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ func NewVoid() Void {
func NewMap(keyType TcValue, valueType TcValue) Map {
return Map{DataType: MAP_TYPE, KeyType: keyType, ValueType: valueType}
}

func NewMaybe(valueType TcValue) Maybe {
return Maybe{DataType: MAYBE_TYPE, MaybeType: valueType}
}
25 changes: 19 additions & 6 deletions analyzer/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func evaluateTypeName(dtype ast.DataType, env *TypeEnvironment) TcValue {
return evalFn(t, env)
case ast.MapType:
return evalMap(t, env)
case ast.MaybeType:
return NewMaybe(evaluateTypeName(t.MaybeType, env))
case ast.UserDefinedType:
return evalUD(t, env)
case nil:
Expand Down Expand Up @@ -178,19 +180,28 @@ func evalMap(analyzedMap ast.MapType, env *TypeEnvironment) TcValue {

func matchTypes(expectedType, providedType TcValue) error {

if expectedType.DType() == builtins.INTERFACE {
errs := checkMethodsImplementations(expectedType, providedType)
unwrappedExpected := unwrapType(expectedType)
unwrappedProvided := unwrapType(providedType)

switch t := unwrappedExpected.(type) {
case Interface:
errs := checkMethodsImplementations(unwrappedExpected, unwrappedProvided)
if len(errs) > 0 {
msgs := fmt.Sprintf("cannot use type '%s' as interface '%s'\n", tcValueToString(providedType), tcValueToString(expectedType))
for _, err := range errs {
msgs += utils.ORANGE.Sprintln(" - " + err.Error())
}
return errors.New(msgs)
}
return nil
case Maybe:
if unwrapType(t.MaybeType).DType() == unwrappedProvided.DType() || unwrappedProvided.DType() == builtins.NULL {
return nil
}
}

expectedStr := tcValueToString(expectedType)
providedStr := tcValueToString(providedType)
expectedStr := tcValueToString(unwrappedExpected)
providedStr := tcValueToString(unwrappedProvided)

if expectedStr != providedStr {
return fmt.Errorf("cannot assign value of type '%s' to type '%s'", providedStr, expectedStr)
Expand All @@ -211,6 +222,8 @@ func tcValueToString(val TcValue) string {
return functionSignatureString(t)
case Map:
return fmt.Sprintf("map[%s]%s", tcValueToString(t.KeyType), tcValueToString(t.ValueType))
case Maybe:
return fmt.Sprintf("maybe{%s}", tcValueToString(t.MaybeType))
case UserDefined:
return tcValueToString(unwrapType(t.TypeDef))
default:
Expand Down Expand Up @@ -248,13 +261,13 @@ func checkMethodsImplementations(expected, provided TcValue) []error {
errs := []error{}

var interfaceType Interface
interfaceType, ok := unwrapType(expected).(Interface)
interfaceType, ok := expected.(Interface)
if !ok {
return []error{fmt.Errorf("type must be an interface")}
}

var structType Struct
structType, ok = unwrapType(provided).(Struct)
structType, ok = provided.(Struct)
if !ok {
return []error{fmt.Errorf("type must be a struct")}
}
Expand Down
1 change: 0 additions & 1 deletion ast/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,6 @@ type StructProp struct {
Value Node
}


type StructLiteral struct {
Identifier IdentifierExpr
Properties map[string]StructProp
Expand Down
23 changes: 22 additions & 1 deletion ast/stmt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ast

import "walrus/lexer"
import (
"walrus/lexer"
)

type ProgramStmt struct {
Contents []Node
Expand Down Expand Up @@ -202,3 +204,22 @@ func (a ImplStmt) StartPos() lexer.Position {
func (a ImplStmt) EndPos() lexer.Position {
return a.Location.End
}

type SafeStmt struct {
Value IdentifierExpr
SafeBlock BlockStmt
UnsafeBlock BlockStmt
Location
}

func (a SafeStmt) INode() {
//empty method implements Node interface
}

func (a SafeStmt) StartPos() lexer.Position {
return a.Location.Start
}

func (a SafeStmt) EndPos() lexer.Position {
return a.Location.End
}
Loading

0 comments on commit a9e68a7

Please sign in to comment.