Bu dillərin əsas xüsusiyyətlərinnən biri Type checking-dir. İlk öncə type checking haqqında danışaq. Compiler-lar type checking prosesini 2 variant ilə edir birincisi dynamic typing digəri isə static typing. Dynamic Type Checking-də compiler/interpreter type checkingi run time edir buda problemlərə type errors gətirib çıxarır ki development-də bunu UB (undefined behavior/runtime errors) adlandıra bilərik. Çünki compiler bu halda type checking run time aparır (Python kimi dillərdə):
x = 1
y = "X"
Buda həm performans itkisi həmdə çoxlu type error-ları ilə nəticələnir.
Static typed dillər isə type checking prosesini compiler time görür və buda öz növbəsində daha yüksək performans və optimizasiya imkanları verir:
Məsələn type safety prosesini static typed dillər aparır. Bu halda compiler dəyişənə aid value validasiya edir və type error-ları maksimum azaltmağa çalışır. Məsələn gcc compiler bəzi yollarla type safety təmin etməyə çalışır yəni biz bir casting və ya punning edilmədikcə biz 2 fərqli object/struct convert və ya assign edə bilmərik. e.g:
#include <stdio.h>
#include <string.h>
int main(){
char chr[0x41];
printf("%d", chr);
}
vmx@kerneldev:~/Documents/mycompiler$ clang-7 -o int int.c
int.c:9:15: warning: format specifies type 'int' but the argument has type 'char *' [-Wformat]
printf("%d", chr);
~~ ^~~
%s
Bu halda format string specifier integer olduğu halda arqumentin character olduğunu görürük bu halda compiler verification stage-də buna imkan vermir (eliminate type errors). Daha secure sayılır bu halda əlbəttə (Baxmayaraq ki C birçox UB/US halları ilə doludur.)
Bu prosesi Compiler parser-dən (Syntax analysis) sonra semantic analiz stage-də edir compiler-in aldığı source code AST (Abstract syntax tree) structure istifadə edərək parser-dən gələn field/token/statement-ləri AST üzərinə parçalayır və type checking prosesini bu stage-də aparır.
Sonra isə compiler type checking və assignment əməliyyatlarını aparır. Golang, Rust, C++ (newer) kimi dillər developer friendly type inference xüsusiyyətinidə əlavə edirlər yəni expression-a aid type-ı avtomatik olaraq detect edə bilirlər məsələn əgər biz GO-da type-sız hər hansı bir variable təyin etsək analyzer avtomatik olaraq təyin olunmuş variable-a aid tipi infer edir.
var i int
j := i // j is an int
in C++
map<int,list<string>>::iterator i = m.begin();
auto i = m.begin(); //inferer type automatically
Gələk Golangın bu işi necə görməsinə go compiler əsas bizə lazım olan 2 stage-dən ibarətdir:
- Parsing
Bu stage-də hər source üçün syntax tree, tokenized (lexical analysis), parsed (syntax analysis) yaradılır.
e.g:
Parse stage:
// Parse parses a single Go source file from src and returns the corresponding
// syntax tree. If there are errors, Parse will return the first error found,
// and a possibly partially constructed syntax tree, or nil.
//
...
func Parse(base *PosBase, src io.Reader, errh ErrorHandler, pragh PragmaHandler, mode Mode) (_ *File, first error) {
...
var p parser
p.init(base, src, errh, pragh, mode) < === Start for parsing
p.next()
return p.fileOrNil(), p.first
}
// ParseFile behaves like Parse but it reads the source from the named file.
func ParseFile(filename string, errh ErrorHandler, pragh PragmaHandler, mode Mode) (*File, error) {
f, err := os.Open(filename)
...
return Parse(NewFileBase(filename), f, errh, pragh, mode)
}
Example tokens:
// Def is the : in :=
Def // :
Not // !
Recv // <-
// precOrOr
OrOr // ||
// precAndAnd
AndAnd // &&
// precCmp
Eql // ==
Neq // !=
Lss // <
Leq // <=
Gtr // >
Geq // >=
Daha sonra 2-ci stage-də Type-checking və AST transformation edir. Burada artıq AST representation hazırlanaq type checking (static) edilir.
Type inference-də bu stage-də baş verir yəni objectin hansı identifier-ə məxsus olduğu detect edilir. Bayaq bildirdiyim kimi infer ediləcək variable-ın tipi yəni. Bu stage gc package-ında baş verir (cmd/compile/internal/gc/typecheck.go)
var _typekind = []string{
TINT: "int",
TUINT: "uint",
TINT8: "int8",
TUINT8: "uint8",
TINT16: "int16",
TUINT16: "uint16",
TINT32: "int32",
TUINT32: "uint32",
TINT64: "int64",
TUINT64: "uint64",
TUINTPTR: "uintptr",
TCOMPLEX64: "complex64",
TCOMPLEX128: "complex128",
TFLOAT32: "float32",
TFLOAT64: "float64",
TBOOL: "bool",
TSTRING: "string",
TPTR: "pointer",
TUNSAFEPTR: "unsafe.Pointer",
TSTRUCT: "struct",
TINTER: "interface",
TCHAN: "chan",
TMAP: "map",
TARRAY: "array",
TSLICE: "slice",
TFUNC: "func",
TNIL: "nil",
TIDEAL: "untyped number",
}
// typecheck type checks node n.
// The result of typecheck MUST be assigned back to n, e.g.
// n.Left = typecheck(n.Left, top)
func typecheck(n *Node, top int) (res *Node) {
...
}
Buradaki prosedurlar çox uzun olduğu üçün çox yazmıram koddan analiz edilə bilər. Daha sonra 3-cü stage var hansı ki burada AST representation-dan SSA (Static single assingment) formasına çevrilir (IR representation). Burada artıq code generation və optimizasiya əməliyyatları aparıla bilir.
Bu stage önəmli stage-dir hansı ki, burada compiler kod generator üçün optimizasiya edir (machine dependent və ya machine indepentd)
Generasiya edilən SSA formasına baxaq:
source code:
package cozloveyou
func optMulADD(a, b, c float64) float64 {
return a*c + b
}
optMulADD <T>
b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr> DEAD
v4 = Addr <*float64> {a} v2 DEAD
v5 = Addr <*float64> {b} v2 DEAD
v6 = Addr <*float64> {c} v2 DEAD
v7 = Addr <*float64> {~r3} v2
v8 = Arg <float64> {a}
v9 = Arg <float64> {b}
v10 = Arg <float64> {c}
v11 = Const64F <float64> [0] DEAD
v12 = Mul64F <float64> v8 v10
v13 = Add64F <float64> v12 v9
v14 = VarDef <mem> {~r3} v1
v15 = Store <mem> {float64} v7 v13 v14
Ret v15
Daha sonra Golang tərəfindən təyin edilmiş optimizasiya rule-ları ilə optimizasiya tətbiq edili (cmd/compile/internal/ssa/gen/genericOps.go).
var genericOps = []opData{
// 2-input arithmetic
// Types must be consistent with Go typing. Add, for example, must take two values
// of the same type and produces that same type.
{name: "Add8", argLength: 2, commutative: true}, // arg0 + arg1
{name: "Add16", argLength: 2, commutative: true},
{name: "Add32", argLength: 2, commutative: true},
{name: "Add64", argLength: 2, commutative: true},
{name: "AddPtr", argLength: 2}, // For address calculations. arg0 is a pointer and arg1 is an int.
{name: "Add32F", argLength: 2, commutative: true},
{name: "Add64F", argLength: 2, commutative: true},
Golang bütün bunları compile time edərkən Python run time (fərqli prinsip-lər ilə) edir. Digər bir məsələ isə önəmli deyərdim concurrency patternidir. Golang built-in gouroutine direktivinə sahibdir hansı ki funksiyaları concurrent icra etmək mümkündür Python-da isə modul ilə edirik və İnterpreter GİL (GLobal Interpreter Lock) mexanizmi ilə güləşə güləşə qalır. Digər bir xüsusiyyəti isə Memory Sharing məsələsidir. Hansı ki, mən həmişə multi-threading istifadə etdikdə thread-lər arasında communication əsas məsələdir. GOlang memory sharing/communication üçün channel istifadə edir hansı ki biz burada thread-lər arasında məlumat mübadiləsini rahatlığla apara bilərik.
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType
Burada isə həm non-blocking (I/O Bound) həmdə blocking (CPU Bound) mexanizmini istifadə edə bilərik. Bu daha çox əməliyyat sistemində tərəfində olan prosesdir (Kernel depended). Bu haqda daha sonra yazaram))))
Golang əsas olaraq sistem tərəfli dil sayılır. Çox vaxt belə fikirlər eşidirəm ki, dil alətdir əsas dil deyil bu əslində belə deyil hər dilin öz xüsusiyyəti var onu necə istifadə edilməsidir əsas məsələ. Yuxarıda qeyd etdiyim 2 faktor mənim üçün əsas məsələdir bunların fərqləri üçün.