39. pyxc: Visibility

What I Am Building

Chapter 38 added constructors. Classes can now be initialized, but every field and method is accessible from anywhere. After this chapter, a class can hide its internals:

extern def printd(x: float64)

class BoundedCounter:
  private count: int
  private limit: int

  def __init__(max: int):
    self.count = 0
    self.limit = max

  public def increment():
    if self.count < self.limit:
      self.count = self.count + 1

  public def get() -> int:
    return self.count


def main() -> int:
  var c: BoundedCounter = BoundedCounter(3)
  c.increment()
  c.increment()
  c.increment()
  c.increment()    # no effect — limit reached
  printd(float64(c.get()))
  return 0
3.000000

Accessing c.count directly from outside the class is an error.

Source Code

git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-39

Grammar

class-member gains an optional visibility prefix. visibility is a new production:

 program                           = [ end-of-lines ]
                                     [ top-level-item
                                       { end-of-lines top-level-item } ]
                                     [ end-of-lines ] ;
 end-of-lines                      = end-of-line { end-of-line } ;
 top-level-item                    = function-definition
                                     | type-alias
                                     | struct-definition
                                     | class-definition
                                     | external
                                     | top-level-statement ;
 struct-definition                 = "struct" name ":" end-of-lines
                                     struct-block ;
 class-definition                  = "class" name ":" end-of-lines
                                     class-block ;
 type-alias                        = "type" name "=" type ;
 struct-block                      = indent field-declaration
                                     { end-of-lines field-declaration } dedent ;
 class-block                       = indent class-member
                                     { end-of-lines class-member } dedent ;
-class-member                      = field-declaration | method-definition ;
+class-member                      = [ visibility ]
+                                    ( field-declaration | method-definition ) ;
+visibility                        = "public" | "private" ;
 field-declaration                 = name ":" type ;
 method-definition                 = "def" name "(" [ parameters ] ")"
                                     [ "->" type ] ":"
                                     ( simple-statement
                                       | end-of-lines block ) ;
 function-definition               = "def" function-signature [ "->" type ] ":"
                                     ( simple-statement
                                       | end-of-lines block ) ;
 external                          = "extern" "def" external-function-signature
                                     [ "->" type ] ;
 top-level-statement               = statement ;
 function-signature                = name "(" [ parameters ] ")" ;
 external-function-signature       = name "(" [ parameters [ "," "..." ] | "..." ] ")" ;
 parameters                        = typed-parameter { "," typed-parameter } ;
 typed-parameter                   = name ":" type ;
 if-statement                      = "if" expression ":" suite
                                     { [ end-of-lines ] "elif" expression ":" suite }
                                     [ [ end-of-lines ] "else" ":" suite ] ;
 for-statement                     = "for" ( "var" name ":" type | name )
                                     "=" expression ","
                                     expression "," expression ":" suite ;
 while-statement                   = "while" expression ":" suite ;
 do-while-statement                = "do" ":" suite [ end-of-lines ]
                                     "while" expression ;
 switch-statement                  = "switch" expression ":" end-of-lines
                                     indent switch-body dedent ;
 switch-body                       = switch-case
                                     { end-of-lines switch-case }
                                     [ end-of-lines default-case ] ;
 switch-case                       = "case" switch-integer
                                     { "," switch-integer } ":" suite ;
 default-case                      = "default" ":" suite ;
 variable-statement                = "var" variable-binding
                                     { "," variable-binding } ;
 simple-statement                  = return-statement
                                     | break-statement
                                     | continue-statement
                                     | variable-statement
                                     | expression ;
 compound-statement                = if-statement
                                     | for-statement
                                     | while-statement
                                     | do-while-statement
                                     | switch-statement ;
 statement                         = simple-statement | compound-statement ;
 suite                             = simple-statement
                                     | compound-statement
                                     | end-of-lines block ;
 return-statement                  = "return" [ expression ] ;
 break-statement                   = "break" ;
 continue-statement                = "continue" ;
 statement-separator               = end-of-lines | BLOCK_END ;
 block                             = indent statement
                                     { statement-separator statement } dedent ;
 expression                        = assignment ;
 assignment                        = logical-or [ assignment-operator assignment ] ;
 logical-or                        = logical-and { "||" logical-and } ;
 logical-and                       = bitwise-or { "&&" bitwise-or } ;
 bitwise-or                        = bitwise-xor { "|" bitwise-xor } ;
 bitwise-xor                       = bitwise-and { "^" bitwise-and } ;
 bitwise-and                       = equality { "&" equality } ;
 equality                          = relational { ("==" | "!=") relational } ;
 relational                        = shift { ("<" | "<=" | ">" | ">=") shift } ;
 shift                             = sum { ("<<" | ">>") sum } ;
 sum                               = term { ("+" | "-") term } ;
 term                              = unary-expression
                                     { ("*" | "/" | "%") unary-expression } ;
 lvalue                            = name
                                     { "." name | "[" expression "]" } ;
 variable-binding                  = name ":" type [ "=" expression ] ;
 unary-expression                  = ("-" | "!" | "~" | "++" | "--")
                                     unary-expression
                                     | postfix-expression ;
 postfix-expression                = primary [ "++" | "--" ] ;
 primary                           = cast-expression
                                     | sizeof-expression
                                     | address-expression
                                     | array-literal
                                     | string-literal
                                     | character-literal
                                     | name-expression
                                     | number-expression
                                     | boolean-literal
                                     | parenthesized-expression ;
 cast-expression                   = cast-type "(" expression ")" ;
 sizeof-expression                 = "sizeof" "(" type ")" ;
 address-expression                = "addr" "(" lvalue ")" ;
 array-literal                     = "[" [ expression
                                       { "," expression } ] "]" ;
 string-literal                    = '"' { string-character | escape } '"' ;
 escape                            = literal-escape ;
 string-character                  = ? any character except '"', "\\", "\r", and "\n" ? ;
 character-literal                 = "'" ( character | character-escape ) "'" ;
 character-escape                  = literal-escape ;
 literal-escape                    = "\\" ( "\\" | "'" | '"' | "?"
                                       | "a" | "b" | "f" | "n" | "r"
                                       | "t" | "v"
                                       | "x" hex-digit hex-digit
                                       | octal-digit [ octal-digit
                                         [ octal-digit ] ]
                                       | "u" hex-digit hex-digit hex-digit hex-digit
                                       | "U" hex-digit hex-digit hex-digit hex-digit
                                         hex-digit hex-digit hex-digit hex-digit ) ;
 character                         = ? any character except "'", "\\", "\r", and "\n" ? ;
 hex-digit                         = digit | "A".."F" | "a".."f" ;
 assignment-operator               = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
 octal-digit                       = "0".."7" ;
 name-expression                   = lvalue
                                     | call-expression
                                     | method-call-expression
                                     | constructor-call-expression ;
 call-expression                   = name "(" [ arguments ] ")" ;
 method-call-expression            = lvalue "." name "(" [ arguments ] ")" ;
 constructor-call-expression       = name "(" [ arguments ] ")" ;
 arguments                         = expression { "," expression } ;
 number-expression                 = number ;
 parenthesized-expression          = "(" expression ")" ;
 indent                            = INDENT ;
 dedent                            = DEDENT ;
 name                              = (letter | "_")
                                     { letter | digit | "_" } ;
 type                              = base-type [ array-suffix ] ;
 base-type                         = builtin-type | alias-type | struct-type
                                     | pointer-type ;
 pointer-type                      = "ptr" "[" type "]" ;
 array-suffix                      = "[" integer "]" ;
 builtin-type                      = "int" | "int8" | "int16" | "int32"
                                     | "int64" | "uint8" | "uint16"
                                     | "uint32" | "uint64"
                                     | "float" | "float32"
                                     | "float64" | "bool" | "None" ;
 struct-type                       = name ;
 alias-type                        = name ;
 cast-type                         = builtin-cast-type | pointer-type ;
 builtin-cast-type                 = "int" | "int8" | "int16" | "int32"
                                     | "int64" | "uint8" | "uint16"
                                     | "uint32" | "uint64"
                                     | "float" | "float32"
                                     | "float64" | "bool" ;
 number                            = ( digit { digit } [ "." { digit } ]
                                     | "." digit { digit } ) [ exponent ] ;
 switch-integer                    = [ "-" ] digit { digit } ;
 exponent                          = ( "e" | "E" ) [ "+" | "-" ]
                                     digit { digit } ;
 boolean-literal                   = "True" | "False" ;
 integer                           = digit { digit } ;
 letter                            = "A".."Z" | "a".."z" ;
 digit                             = "0".."9" ;
 end-of-line                       = "\r\n" | "\r" | "\n" ;
 (*
     A `comment` begins with "#" and continues to the end of the line. The lexer
      ignores its text and returns an end-of-line token when one follows it.
 *)
 comment                           = "#" { comment-character } ;
 comment-character                 = ? any character except "\r" and "\n" ? ;
 (*
     `whitespace` may appear before or between tokens
      and is ignored by the lexer.
 *)
 whitespace                        = " " | "\t" | "\v" | "\f" ;
 INDENT                            = ? synthetic token emitted by lexer when indentation increases ? ;
 DEDENT                            = ? synthetic token emitted by lexer when indentation decreases ? ;
 BLOCK_END                         = ? synthetic token injected into the stream by ParseBlock
                                       immediately after it consumes DEDENT ? ;

New Tokens

tok_public = -65,
tok_private = -66,

Both are registered in the keyword table and in the token-name map, so error messages print 'public' and 'private'.

Storing Visibility Information

Visibility lands in two places on StructTypeInfo. Fields gain an IsPublic flag directly on StructFieldInfo:

struct StructFieldInfo {
  string Name;
  ValueType Type;
  string StructName;
  bool IsPublic = true;
};

Methods are tracked separately, in a map from method name to visibility:

struct StructTypeInfo {
  vector<StructFieldInfo> Fields;
  map<string, size_t> FieldIndices;
  map<string, bool> Methods;
  bool IsClass = false;
};

Methods need their own map rather than a flag next to the field, since a method's signature lives in FunctionSignatures, not in StructTypeInfo::Fields — there's no single struct visibility could hang off of the way IsPublic hangs off StructFieldInfo.

Parsing Visibility Modifiers

The body loop inside ParseAggregateDefinition now reads an optional visibility token before deciding whether the member is a field or a method:

bool IsPublic = true;
if (CurrentToken == tok_public || CurrentToken == tok_private) {
  if (!Info.IsClass) {
    LogErrorExpression(
        "Visibility modifiers are only allowed inside class bodies");
    return false;
  }
  IsPublic = CurrentToken == tok_public;
  getNextToken(); // eat visibility modifier
}

With no modifier, IsPublic stays true: the default is public. A modifier inside a struct body is rejected immediately, before the parser even eats the token, let alone looks at what follows it.

A field's visibility rides along with everything else already pushed into Info.Fields:

Info.Fields.push_back(
    {FieldName, FieldType, FieldStructName, IsPublic});

A method's visibility is passed as an extra argument into ParseMethodDefinition, which now takes bool IsPublic and records it directly:

StructTypes[ClassName].Methods[MethodName] = IsPublic;

Info.Methods is copied back out of StructTypes[AggregateName] after every method (Info.Methods = StructTypes[AggregateName].Methods;), for the same reason Chapter 37 already re-registers StructTypes[AggregateName] = Info after every member: methods parsed earlier in the body need to stay visible while later members are parsed, and vice versa.

Enforcing Private Access

Access is decided by one small function:

static string CurrentClassScopeName;

static bool CanAccessClassMember(const string &OwnerClass, bool IsPublic) {
  return IsPublic || (!CurrentClassScopeName.empty() &&
                      CurrentClassScopeName == OwnerClass);
}

A member is reachable if it's public, or if the code currently being parsed belongs to the same class the member is on. "Currently being parsed" is CurrentClassScopeName, set and restored by an RAII guard:

struct ClassScopeGuard {
  string SavedClassName;
  ClassScopeGuard(const string &ClassName)
      : SavedClassName(CurrentClassScopeName) {
    CurrentClassScopeName = ClassName;
  }
  ~ClassScopeGuard() { CurrentClassScopeName = SavedClassName; }
};

ParseMethodDefinition instantiates a ClassScopeGuard before parsing the method's body. When the method is done, the destructor restores whatever CurrentClassScopeName was before — empty at the top level, since pyxc has no nested classes to restore into instead.

Access Checks at Every Use Site

CanAccessClassMember is checked wherever the parser resolves a class member, which turns out to be four places, not one:

Field access, in both places a .field chain gets resolved. ParseFieldExpressionWithBase walks a chain that starts from an already-typed base (used for the auto-deref case, where self is ptr[SomeStruct] and the pointee is decoded before the walk begins):

const auto &Field = Struct->second.Fields[FieldIndex->second];
if (!CanAccessClassMember(ResultStructName, Field.IsPublic))
  return LogErrorExpression(
      ("Field '" + FieldName + "' is private on '" +
       ResultStructName + "'")
          .c_str());

ParseNameExpressionWithName has its own inline dot-dispatch loop for the ordinary name.field case, with the identical check inline:

const auto &FieldInfo = Struct->second.Fields[Field->second];
if (!CanAccessClassMember(BaseStructName, FieldInfo.IsPublic))
  return LogErrorExpression(
      ("Field '" + MemberName + "' is private on '" +
       BaseStructName + "'")
          .c_str());

Both paths reject reading and writing a private field, since assignment and read both resolve the field chain through one of these two places before anything else happens.

Method call, in ParseMethodCallExpression, right after resolving ClassName.MethodName:

auto Visibility = Class->second.Methods.find(MethodName);
if (Visibility != Class->second.Methods.end() &&
    !CanAccessClassMember(ClassName, Visibility->second))
  return LogErrorExpression(
      ("Method '" + MethodName + "' is private on '" + ClassName + "'")
          .c_str());

Constructor call, in ParseNameExpressionWithName, guarded by whether __init__ exists at all:

if (Initializer) {
  auto Visibility = Class->second.Methods.find("__init__");
  if (Visibility != Class->second.Methods.end() &&
      !CanAccessClassMember(ParsedName, Visibility->second))
    return LogErrorExpression(
        ("Method '__init__' is private on '" + ParsedName + "'").c_str());
}

A private __init__ makes ClassName(args) fail from outside the class, the same way a private method or field would.

IR Is Unchanged

Visibility is enforced entirely while parsing. Nothing changes in the generated IR: public and private leave no trace in the output. A private field and a public field of the same type generate identical IR.

Known Limitations

There is no protected. Access is either class-private or world-public; there's no subclass-visible middle tier, since pyxc has no inheritance.

Visibility modifiers on structs are rejected outright. struct members are always public. The parser errors the moment it sees public or private before a struct member, rather than silently ignoring the modifier.

Build and Run

cd code/chapter-39
cmake -S . -B build && cmake --build build
./build/pyxc
llvm-lit -v test/

Try It

Private field, accessed from outside

class Foo:
  private x: int

def main() -> int:
  var f: Foo
  f.x = 3
  return 0
Error (Line 6, Column 7): Field 'x' is private on 'Foo'

Private constructor, called from outside

class Foo:
  x: int
  private def __init__():
    self.x = 0

def main() -> int:
  var f: Foo = Foo()
  return 0
Error (Line 7, Column 20): Method '__init__' is private on 'Foo'

Visibility modifier on a struct

struct Foo:
  private x: int
Error (Line 2, Column 3): Visibility modifiers are only allowed inside class bodies

What's Next

Chapter 40 adds traits.

Need Help?

Build issues? Questions?

Include:

  • Your OS and version
  • Full error message
  • Output of cmake --version, ninja --version, and llvm-config --version

I'll help you figure it out.