這個scheme語言實現中,所有的東西都是一個對象,這就是它的類型系統.
一個對象可能是一個立即值也可能是一個sexp.由於通過各種tag識別出一個對象真實的類型.
現在只要明白,32位機器上,對象就是一個32位的黑箱.
要麼這32位本身存了它的真實資料,這種就是立即值.
要麼這是一個指標,這種就是一個sexp.
第一部分:immediate(立即值)
立即值包括整數,char,唯一的立即符號等.
我們知道,一個位元組是8位,所以一個指標的最後三個bit一定是0.立即數就是利用了這一點,將這三位作為tag位.
一個對象,也就是這個32位的東西,先識別其最後3個bit,如果不是全0,它就是一個立即值,否則它是一個指標.
檔案./include/chibi/sexp.h中的注釋
/* tagging system
* bits end in 00: pointer
* 01: fixnum
* 011: immediate flonum (optional)
* 111: immediate symbol (optional)
* 000110: char
* 001110: unique immediate (NULL, TRUE, FALSE)
*/
因此可以很容易的知道一個對象是否為立即值
#define SEXP_FIXNUM_MASK 3
#define SEXP_IMMEDIATE_MASK 7
#define SEXP_EXTENDED_MASK 63
#define SEXP_POINTER_TAG 0
#define SEXP_FIXNUM_TAG 1
#define SEXP_ISYMBOL_TAG 7
#define SEXP_CHAR_TAG 6
#define sexp_pointerp(x) (((sexp_uint_t)(x) & SEXP_FIXNUM_MASK) == SEXP_POINTER_TAG)
#define sexp_fixnump(x) (((sexp_uint_t)(x) & SEXP_FIXNUM_MASK) == SEXP_FIXNUM_TAG)
#define sexp_isymbolp(x) (((sexp_uint_t)(x) & SEXP_IMMEDIATE_MASK) == SEXP_ISYMBOL_TAG)
#define sexp_charp(x) (((sexp_uint_t)(x) & SEXP_EXTENDED_MASK) == SEXP_CHAR_TAG)
sexp_uint_t就是unsigned int,32位機器上就是一個32位的東西.
這裡的整型只使用了其中的30位,也就是表示的範圍是0-2的30次方減1(如果是無符),大多數時候30位的數都夠用的.
另外,chibi-scheme還實現了大數,可以支援更大的數,具體可以去看原始碼,本文不做具體分析.
第二部分:sexp(s運算式)
如果一個對象不是立即值(最後3bit不為0),它就是一個sexp.
typedef struct sexp_struct *sexp;
sexp_struct是由一個頭部加上一個union的各種類型的值組成的.頭部tag欄位用來確定類型,markedp等等欄位是為記憶體回收設定的.
struct sexp_struct {
sexp_tag_t tag;
char markedp;
......
union {
double flonum ;
struct {
sexp car, cdr;
sexp source;
} pair;
struct {
sexp_uint_t length;
sexp data[];
} vector;
......
}value;
};
除了立即值,所有的chibi-scheme使用的類型都被抽象成了sexp,不限於scheme語言所定義的基礎資料型別 (Elementary Data Type),還包括了運行時類型如堆,上下文,還有ast樹的結構如SEXP_CND, SEXP_REF, SEXP_SET, SEXP_SEQ, SEXP_LIT,等都是sexp.
sexp_tag_t的那個tag可取值為下面的枚舉:
enum sexp_types {
SEXP_OBJECT,
SEXP_TYPE,
SEXP_FIXNUM,
SEXP_NUMBER,
SEXP_CHAR,
SEXP_BOOLEAN,
......
SEXP_IPORT,
SEXP_OPORT,
SEXP_EXCEPTION,
SEXP_PROCEDURE,
SEXP_MACRO,
SEXP_SYNCLO,
SEXP_ENV,
SEXP_BYTECODE,
SEXP_CORE,
SEXP_OPCODE,
SEXP_LAMBDA,
SEXP_CND,
SEXP_REF,
SEXP_SET,
SEXP_SEQ,
SEXP_LIT,
SEXP_STACK,
SEXP_CONTEXT,
SEXP_CPOINTER,
SEXP_NUM_CORE_TYPES
};第三部分:sexp相關操作
為了更方便在操作,./include/chibi/sexp.h中提供了許多的宏,提供類型的謂詞判斷,空間分配,各種類型各個域成員的訪問宏.
有些複雜一些的在sexp.h中只是一個函式宣告,實現放在sexp.c和gc.c中.
還有一些是scheme對象的算術運算宏,以及各類型大小計算的宏.例如下面是其中一部分原始碼片斷:
#define sexp_check_tag(x,t) (sexp_pointerp(x) && (sexp_pointer_tag(x) == (t)))
#define sexp_typep(x) (sexp_check_tag(x, SEXP_TYPE))
#define sexp_pairp(x) (sexp_check_tag(x, SEXP_PAIR))
#define sexp_stringp(x) (sexp_check_tag(x, SEXP_STRING))
...
#define sexp_field(x, type, id, field) ((x)->value.type.field)
#define sexp_pred_field(x, type, pred, field) ((x)->value.type.field)
#define sexp_vector_length(x) (sexp_field(x, vector, SEXP_VECTOR, length))
#define sexp_vector_data(x) (sexp_field(x, vector, SEXP_VECTOR, data))
...
#define sexp_string_length(x) (sexp_field(x, string, SEXP_STRING, length))
#define sexp_string_data(x) (sexp_field(x, string, SEXP_STRING, data))
...
注意到sexp的資料部分是用union定義的,union的佔用的記憶體大小是其中最大的成員小大小.這樣豈不會造成很大的記憶體浪費?
後面我注意到,在實際的記憶體配置時不是分配sizeof(union)的大小,而是對每種類型分配其真實大小.
#define sexp_sizeof(x) (offsetof(struct sexp_struct, value) \
+ sizeof(((sexp)0)->value.x))
比如sexp_make_flonum建立一個浮點數對象,其調用鏈是這樣的:
sexp_make_flonum(sexp ctx , double f)
==> sexp_alloc_type(ctx,flonum,SEXP_FLONUM)
==> sexp_alloc_tagged(ctx,sexp_sizeof(flonum),SEXP_FLONUM)
==> sexp_alloc_tagged_aux
==> sexp_alloc(ctx,size)
以上各函數的聲明都在sexp.h中,sexp_alloc函數的實現在檔案gc.c中,其作用是在上下文中分配size位元組大小的空間.
第四部分:類型對象
scheme是動態類型語言,這裡所謂"動態",按我的理解是符號本身是沒有類型的,但符號綁定的值是有類型的。一個符號可以綁定到任意類型的值,所以是動態。這裡可能用英語單詞更精確,我所說的符號,更傾向於是identifier,或者symbol,甚至就叫name,但不是指variable。
(define a 100) ;;a是一個符號,符號綁定到整型的值100
(define a "hello world") ;;a被綁定到一個字串,注意,a本身是不具有類型的
值是有類型的,100這個值是整型。“hello world”這個值是一個字串。僅僅一個tag域,想要獲得全部的運行時類型資訊顯然是不夠的。比如說,我想知道一個對象佔用的真實位元組是多少。我很容易通過宏sexp_pointer_tag()得到對象的tag。但想使用宏sexp_sizeof(tag)得到大小是會報錯的,因為參數需要是一個類型比如flonum,但tag只是一個編號,放在這個具體情況,tag只是代表flonum類型的編號。
很自然地聯想到,需要一張全域的表,將類型編號和類型的真實資訊聯絡起來。
這張表中的對象,就是類型對象,類型對象用來記錄每種類型的具體資訊。chibi-scheme中,這個設計確實存在。
sexp的union其中的一個是type對象:
struct sexp_struct{
...
union {
...
struct sexp_type_struct type;
...
}values;
};
結構體sexp_type_struct就是用來記錄各種類型的資訊的:
struct sexp_type_struct {
sexp_tag_t tag;
short field_base, field_eq_len_base, field_len_base, field_len_off;
unsigned short field_len_scale;
short size_base, size_off;
unsigned short size_scale;
short weak_base, weak_len_base, weak_len_off, weak_len_scale, weak_len_extra;
short depth;
sexp name, cpl, slots, dl, id, print;
sexp_proc2 finalize;
};
這張表也是真實存在的,它其實是context->globals[SEXP_G_TYPES]。這裡姑且簡單的使用table來表示。
舉個的例子,假設有一個對象x,先通過sexp_pointer_tag(x)得到其tag,再通過table[tag]就可查到x的類型對象了。
再對這個類型對象進行操作就可以知道x的全部類型資訊,這些操作都是封裝好了的。
#define sexp_object_type(ctx,x) (sexp_type_by_index(ctx, ((x)->tag)))
#define sexp_type_by_index(ctx,i) (sexp_context_types(ctx)[i])
#define sexp_context_types(ctx) sexp_vector_data(sexp_global(ctx, SEXP_G_TYPES))
#define sexp_global(ctx,x) (sexp_vector_data(sexp_context_globals(ctx))[x])
小結
本篇講解了chibi-scheme的類型系統.主要代碼對應於檔案./include/chibi/sexp.h
類型系統非常重要,所有的scheme對象在本質上就是一個32位的黑箱,它或者是一個立即值,或者是一個s運算式.
立即值中的一些bits可作為標識區別出是哪種立即值,而s運算式則是一個指向結構體的指標,結構體頭部含有的tag決定其類型.
sexp.h中提供的宏為操作s運算式提供了非常好的支援,在後面的程式中可以很方便地使用,它同時也是整個程式都要include的標頭檔.
這些操作具體包括了類型的判斷謂詞,大小,各個類型各個域的訪問宏,各種類型的建立等等.
僅僅通過tag擷取全部運行時類型資訊是不夠的。對應的,有類型對象是專門來存各種各樣的類型資訊的,包括類型名稱,類型佔用位元組數,類型的對齊,類型在gc是否調用某個解構函式等等。還有一張表,表中存的各種類型對象。有了這張表,就可以把tag和類型對象關聯起來了。