27. pyxc: Visibility
Where We Are
Chapter 26 added constructors. Classes can now be initialised, 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 would be an error.
Source Code
git clone --depth 1 https://github.com/alankarmisra/pyxc-llvm-tutorial
cd pyxc-llvm-tutorial/code/chapter-27
Grammar
classmember gains an optional visibility prefix. visibility is a new production.
classmember = [ visibility ] ( fielddecl | methoddef ) ; -- changed
visibility = "public" | "private" ; -- new
Full Grammar
code/chapter-27/pyxc.ebnf
program = [ eols ] [ top { eols top } ] [ eols ] ;
eols = eol { eol } ;
top = typealias | structdef | classdef | definition | decorateddef | external | toplevelexpr ;
typealias = "type" identifier "=" type ;
structdef = "struct" identifier ":" eols structblock ;
classdef = "class" identifier ":" eols structblock ;
structblock = indent classmember { eols classmember } dedent ;
classmember = [ visibility ] ( fielddecl | methoddef ) ;
visibility = "public" | "private" ;
methoddef = "def" identifier "(" [ typedparam { "," typedparam } ] ")"
[ "->" type ] ":" ( simplestmt | eols block ) ;
fielddecl = identifier ":" type ;
definition = "def" prototype [ "->" type ] ":" ( simplestmt | eols block ) ;
decorateddef = binarydecorator eols "def" binaryopprototype [ "->" type ] ":" ( simplestmt | eols block )
| unarydecorator eols "def" unaryopprototype [ "->" type ] ":" ( simplestmt | eols block ) ;
binarydecorator = "@" "binary" "(" integer ")" ;
unarydecorator = "@" "unary" ;
binaryopprototype = customopchar "(" typedparam "," typedparam ")" ;
unaryopprototype = customopchar "(" typedparam ")" ;
external = "extern" "def" prototype [ "->" type ] ;
toplevelexpr = expression ;
prototype = identifier "(" [ typedparam { "," typedparam } ] ")" ;
typedparam = identifier ":" type ;
ifstmt = "if" expression ":" suite
[ eols "else" ":" suite ] ;
forstmt = "for"
( "var" identifier ":" type | identifier )
"=" expression "," expression "," expression ":" suite ;
varstmt = "var" varbinding { "," varbinding } ;
assignstmt = lvalue "=" expression ;
simplestmt = returnstmt | varstmt | assignstmt | expression ;
compoundstmt = ifstmt | forstmt ;
statement = simplestmt | compoundstmt ;
suite = simplestmt | compoundstmt | eols block ;
returnstmt = "return" [ expression ] ;
block = indent statement { eols statement } dedent ;
expression = unaryexpr binoprhs ;
binoprhs = { binaryop unaryexpr } ;
lvalue = identifier | fieldaccess | indexexpr ;
varbinding = identifier ":" type [ "=" expression ] ;
unaryexpr = unaryop unaryexpr | primary ;
unaryop = "-" | userdefunaryop ;
primary = castexpr | sizeofexpr | addrexpr | arrayliteral | stringliteral | identifierexpr | fieldaccess | indexexpr | numberexpr | bool_literal | parenexpr ;
castexpr = casttype "(" expression ")" ;
sizeofexpr = "sizeof" "(" type ")" ;
addrexpr = "addr" "(" lvalue ")" ;
identifierexpr = identifier | callexpr | methodcallexpr | ctorcallexpr ;
callexpr = identifier "(" [ expression { "," expression } ] ")" ;
methodcallexpr = identifier "." identifier "(" [ expression { "," expression } ] ")" ;
ctorcallexpr = identifier "(" [ expression { "," expression } ] ")" ;
fieldaccess = identifier "." identifier { "." identifier } ;
indexexpr = identifier "[" expression "]" ;
numberexpr = number ;
arrayliteral = "[" [ expression { "," expression } ] "]" ;
stringliteral = "\"" { ? any char except " and newline ? | escape } "\"" ;
escape = "\\" ( "\\" | "\"" | "n" | "t" | "0" ) ;
parenexpr = "(" expression ")" ;
binaryop = builtinbinaryop | userdefbinaryop ;
indent = INDENT ;
dedent = DEDENT ;
builtinbinaryop = "+" | "-" | "*" | "<" | "<=" | ">" | ">=" | "==" | "!=" ;
userdefbinaryop = ? any opchar defined as a custom binary operator ? ;
userdefunaryop = ? any opchar defined as a custom unary operator ? ;
customopchar = ? any opchar that is not "-" or a builtinbinaryop,
and not already defined as a custom operator ? ;
opchar = ? any single ASCII punctuation character ? ;
identifier = (letter | "_") { letter | digit | "_" } ;
builtintype = "int" | "int8" | "int16" | "int32" | "int64"
| "float" | "float32" | "float64"
| "bool" | "None" ;
aliastype = identifier ;
structtype = identifier ;
pointertype = "ptr" "[" type "]" ;
type = basetype [ arraysuffix ] ;
basetype = builtintype | aliastype | structtype | pointertype ;
arraysuffix = "[" integer "]" ;
casttype = "int" | "int8" | "int16" | "int32" | "int64"
| "float" | "float32" | "float64"
| "bool" | pointertype ;
integer = digit { digit } ;
number = digit { digit } [ "." { digit } ]
| "." digit { digit } ;
bool_literal = "True" | "False" ;
letter = "A".."Z" | "a".."z" ;
digit = "0".."9" ;
eol = "\r\n" | "\r" | "\n" ;
ws = " " | "\t" ;
INDENT = ? synthetic token emitted by lexer ? ;
DEDENT = ? synthetic token emitted by lexer ? ;
New Tokens
tok_public = -41,
tok_private = -42,
Both are registered in the keyword table:
{"public", tok_public},
{"private", tok_private},
They are also added to the token name map so error messages print 'public' and 'private'.
Storing Visibility in StructTypeInfo
Visibility is stored on two places in StructTypeInfo:
Fields gain an IsPublic flag:
struct FieldInfo {
string Name;
ValueType Type;
string StructName;
bool IsPublic = true; // new — default public
};
Methods are tracked in a map from method name to boolean:
struct StructTypeInfo {
// ...
std::map<string, bool> MethodIsPublic; // new
};
Methods use a map rather than a flag on the prototype because the prototype lives in FunctionProtos and visibility is class metadata, not function metadata.
Parsing Visibility Modifiers
In ParseAggregateDefinition, the body loop now reads an optional visibility token before each member:
bool MemberIsPublic = true;
bool HasVisibilityModifier = false;
if (CurTok == tok_public || CurTok == tok_private) {
HasVisibilityModifier = true;
MemberIsPublic = (CurTok == tok_public);
getNextToken(); // eat visibility modifier
}
if (HasVisibilityModifier && !Info.IsClass) {
LogError("Visibility modifiers are only allowed inside class bodies");
return false;
}
If no modifier is present, MemberIsPublic stays true — the default is public. If the modifier appears inside a struct body, it is rejected immediately.
After parsing a field, the visibility is stored in FieldInfo:
Info.Fields.push_back({FieldName, FieldType, FieldStructName, MemberIsPublic});
After parsing a method, the method's visibility is registered in MethodIsPublic by ParseMethodDefinitionInClass (which now takes bool IsPublic as a parameter):
StructTypes[ClassName].MethodIsPublic[MethodName] = IsPublic;
The StructTypes[StructName] entry is written back after each member — Info.MethodIsPublic = StructTypes[StructName].MethodIsPublic — so the running map is always current as parsing proceeds.
CanAccessClassMember and ClassScopeGuard
Access is decided by a single function:
static string CurrentClassScopeName;
static bool CanAccessClassMember(const string &OwnerClass, bool IsPublic) {
return IsPublic || (!CurrentClassScopeName.empty() &&
CurrentClassScopeName == OwnerClass);
}
A member is accessible if it is public, or if the code currently being compiled belongs to the same class. "Currently being compiled" is tracked by CurrentClassScopeName.
ClassScopeGuard sets and restores CurrentClassScopeName around method codegen:
struct ClassScopeGuard {
string Saved;
ClassScopeGuard(const string &ClassName) : Saved(CurrentClassScopeName) {
CurrentClassScopeName = ClassName;
}
~ClassScopeGuard() { CurrentClassScopeName = Saved; }
};
ParseMethodDefinitionInClass creates a ClassScopeGuard before entering the body. When the method is done, the destructor restores the previous class scope (which is "" at the top level, or the enclosing class if methods are somehow nested — though pyxc does not currently support nested classes).
Access Checks at Every Use Site
CanAccessClassMember is inserted at every point where the compiler touches a class member:
Field access — in the ConsumeField lambda inside ParseFieldAccessFromFirstMember:
if (!CanAccessClassMember(CurStruct, FD.IsPublic))
return LogError(("Field '" + Field + "' is private on '" + CurStruct + "'").c_str());
This fires for both read (obj.x) and write (obj.x = v) paths, because both go through ParseFieldAccessFromFirstMember.
Method call — in ParseMethodCallExpr, after looking up ClassName.MethodName:
auto MI = CI->second.MethodIsPublic.find(MethodName);
if (MI != CI->second.MethodIsPublic.end() &&
!CanAccessClassMember(ClassName, MI->second)) {
return LogError(("Method '" + MethodName + "' is private on '" + ClassName + "'").c_str());
}
Constructor call — in ParseIdentifierExpr, if __init__ exists:
auto MI = SI->second.MethodIsPublic.find("__init__");
if (MI != SI->second.MethodIsPublic.end() &&
!CanAccessClassMember(IdName, MI->second)) {
return LogError(("Method '__init__' is private on '" + IdName + "'").c_str());
}
IR Is Unchanged
Visibility is enforced entirely at parse and semantic check time. Nothing changes in the generated IR — public and private leave no trace in the output. A private int and a public int generate identical i64 fields.
Things Worth Knowing
Default is public. A member without a modifier is public. Existing code from chapters 25 and 26, which has no modifiers, continues to work exactly as before.
private __init__ prevents external construction. If __init__ is private, ClassName(args) from outside the class body is rejected.
There is no protected. Access is either class-private or world-public. No inheritance hierarchy, no friend declarations.
Visibility modifiers on structs are rejected. struct members are always public. The parser errors immediately if it sees public or private in a struct body.
What's Next
Chapter 28 adds traits — named contracts that a class can declare it satisfies. Conformance is checked at compile time.
Need Help?
Build issues? Questions?
- GitHub Issues: Report problems
- Discussions: Ask questions
Include:
- Your OS and version
- Full error message
- Output of
cmake --version,ninja --version, andllvm-config --version
We'll figure it out.