[Bootstrap] Add the support for keywords

Both normal and ns qualified keywords with support for ns aliased
keywords.
This commit is contained in:
Sameer Rahmani 2020-12-22 23:54:21 +00:00
parent 4530f35e9c
commit 3f6b9b6e29
7 changed files with 251 additions and 18 deletions

View File

@ -39,6 +39,18 @@ func evalForm(rt *Runtime, scope IScope, form IExpr) (IExpr, IError) {
case ast.String:
return form, nil
// Keyword evaluation rules:
// * Keywords evaluates to themselves with respect to a
// possible namespace alias. For example `::core/xyz`
// will evaluates to `:serene.core/xyz` only if the ns
// `serene.core` is loaded in the current ns with the
// `core` alias. Also `::xyz` will evaluete to
// `:<CURRENT_NS>/xyz`
case ast.Keyword:
// Eval initialize the keyword and MUTATES the state of the keyword
// and returns the updated keyword which would be the same
return form.(*Keyword).Eval(rt, scope)
// Symbol evaluation rules:
// * If it's a NS qualified symbol (NSQS), Look it up in the external symbol table of
// the current namespace.

View File

@ -18,18 +18,200 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
// type Keyword struct{
// Node
// }
// Keyword implementation:
// IMPORTANT NOTE: This implementation keyword is not decent at all
// it lacks many aspects of keywords which makes them great. But
// it is good enough for our use case. So we'll leave it as it is.
//
// Keywords are simple names and just names and nothing more. They
// match the following grammar:
//
// ```
// KEYWORD = COLON [COLON] SYMBOL
// ```
// Normally keywords doesn't have any namespace for example `:xyz`
// is just a name, but in order to avoid any name collision Serene
// supports namespace qualified keywords which basically put a keyword
// under a namespace. But it doesn't mean that you need to load a
// namespace in order to use any keyword that lives under that ns.
// because keywords are just names remember? There is two ways to
// use namespace qualified keywords:
//
// 1. Using full namespace name. For example, `:serene.core/xyz`
// To use this keyword you don't need the namespace `serene.core`
// to be loaded. It's just a name after all.
//
// 2. Using aliased namespaces. For example `::xyz` or `::core/xyz`.
// Using two colons instructs Serene to use aliased namespaces
// with the keyword. In the `::xyz` since the ns part is missing
// Serene will use the current namespace. For instance:
//
// ```
// user> ::xyz
// :user/xyz
// ```
// As you can see `::xyz` and `:user/xyz` (`user` being the ns name)
// are literally the same.
//
// But if we provide the ns part (`::core/xyz` example), a namespace
// with that alias has to be loaded and present in the current
// namespace. For example:
//
// ```
// user> (require '(examples.hello-world hello))
// <ns: examples.hello-world at /home/lxsameer/src/serene/serene/bootstrap/examples/hello-world.srn>
// user> ::hello/xyz
// :examples.hello-world/xyz
// ```
// As you can see we had to load the ns with the `hello` alias to be
// able to use the alias in a keyword.
//
// TODO: Cache the keywords in the runtime on the first eval so we
// done have to evaluate them over and over agian. It can be achieved
// by caching the `hash` value in the keyword itself and maintain a
// hashmap in the runtime from hash codes to a pointer to the keyword.
// But garbage collecting it would be an issue since Golang doesn't support
// weak pointer, but since bootstrap version of Serene is used only to
// bootstrap the compiler it's ok to ignore that for now
// func (n *Keyword) GetType() ast.NodeType {
// return ast.Keyword
// }
import (
"fmt"
"strings"
// func (n NilType) String() string {
// return ""
// }
"serene-lang.org/bootstrap/pkg/ast"
"serene-lang.org/bootstrap/pkg/hash"
)
// func (n NilType) ToDebugStr() string {
// return "nil"
// }
type Keyword struct {
Node
name string
// nsName is the string that is used as the namespace name. It
// might be an ns alias in the current ns or the full namespace
// as well. The first time that this keyword gets evaluated the
// `ns` field will be populated by a pointer to the actual
// namespace which is referrenced to via `nsName` and after that
// nsName will be pretty much useless.
nsName string
// It will be populated after the first evaluation of this keyword
ns *Namespace
// Is it like :serene.core/something
nsQualified bool
// Is it like ::s/something ?
aliased bool
}
func (k *Keyword) GetType() ast.NodeType {
return ast.Keyword
}
func (k *Keyword) String() string {
if k.nsQualified {
if k.ns == nil {
return ":" + k.nsName + "/" + k.name
}
return ":" + k.ns.GetName() + "/" + k.name
}
return ":" + k.name
}
func (k *Keyword) ToDebugStr() string {
var ns string
if k.nsQualified {
ns = k.ns.GetName() + "/"
} else {
ns = ""
}
return fmt.Sprintf("<keword :%s%s at %p>", ns, k.name, k)
}
func (k *Keyword) Hash() uint32 {
bytes := []byte(k.name)
nameHash := hash.HashOf(append([]byte{byte(ast.Keyword)}, bytes...))
if k.nsQualified {
if k.ns != nil {
return hash.CombineHashes(hash.HashOf([]byte(k.ns.GetName())), nameHash)
}
}
return nameHash
}
func (k *Keyword) SetNS(ns *Namespace) {
k.ns = ns
}
func (k *Keyword) IsNSQualified() bool {
return k.nsQualified
}
// Eval initializes the keyword by looking up the possible
// alias name and set it in the keyword.
func (k *Keyword) Eval(rt *Runtime, scope IScope) (*Keyword, IError) {
if k.nsQualified && k.aliased {
aliasedNS := rt.CurrentNS()
if k.nsName != "" {
aliasedNS = rt.CurrentNS().LookupExternal(k.nsName)
}
if aliasedNS == nil {
return nil, MakeErrorFor(rt, k, fmt.Sprintf("can't find the alias '%s' in the current namespace.", k.nsName))
}
k.ns = aliasedNS
return k, nil
}
return k, nil
}
// Extracts the different parts of the keyword
func extractParts(s string) (string, string) {
parts := strings.Split(s, "/")
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", parts[0]
}
func MakeKeyword(n Node, name string) (*Keyword, IError) {
if strings.Count(name, ":") > 2 {
return nil, MakeParsetimeErrorf(n, "can't parse the keyword with more that two colons: '%s'", name)
}
if strings.Count(name, "/") > 1 {
return nil, MakeParsetimeErrorf(n, "illegal namespace path for the given keyword: '%s'", name)
}
var nsName string
var kwName string
keyword := name
nsQualified := false
aliased := false
if strings.HasPrefix(name, "::") {
nsQualified = true
aliased = true
keyword = name[2:]
} else if strings.HasPrefix(name, ":") && strings.Count(name, "/") == 1 {
nsQualified = true
keyword = name[1:]
} else if strings.HasPrefix(name, ":") {
keyword = name[1:]
}
nsName, kwName = extractParts(keyword)
return &Keyword{
Node: n,
name: kwName,
nsName: nsName,
nsQualified: nsQualified,
aliased: aliased,
}, nil
}

View File

@ -111,6 +111,16 @@ func (n *Namespace) hasExternal(nsName string) bool {
return ok
}
// LookupExternal looks up the given `alias` in the `externals` table
// of the namespace.
func (n *Namespace) LookupExternal(alias string) *Namespace {
if n.hasExternal(alias) {
return n.externals[alias]
}
return nil
}
func (n *Namespace) setExternal(name string, ns *Namespace) {
n.externals[name] = ns
}

View File

@ -160,6 +160,16 @@ func isValidForSymbol(char string) bool {
return contains(validChars, c) || unicode.IsLetter(c) || unicode.IsDigit(c)
}
func readKeyword(parser IParsable) (IExpr, IError) {
symbol, err := readRawSymbol(parser)
if err != nil {
return nil, err
}
node := MakeNodeFromExpr(symbol)
return MakeKeyword(node, ":"+symbol.(*Symbol).String())
}
//readRawSymbol reads a symbol from the current position forward
func readRawSymbol(parser IParsable) (IExpr, IError) {
c := parser.peek(false)
@ -197,7 +207,6 @@ func readRawSymbol(parser IParsable) (IExpr, IError) {
}
}
// TODO: Add support for ns qualified symbols
node := MakeNode(parser.Buffer(), parser.GetLocation()-len(symbol), parser.GetLocation())
sym, err := MakeSymbol(node, symbol)
@ -466,12 +475,17 @@ loop:
readComment(parser)
goto loop
}
// case '[':
if *c == ":" {
return readKeyword(parser)
}
// if *c == "[" {
// readVector(parser)
// }
// case '{':
// if *c == "{" {
// readMap(parser)
// }
parser.back()
return readSymbol(parser)

View File

@ -136,9 +136,11 @@ func (r *Runtime) IsQQSimplificationEnabled() bool {
func nsNameToPath(ns string) string {
replacer := strings.NewReplacer(
".", "/",
// TODO: checkout the different OSs for character supports in
// the filesystem level
//"-", "_",
)
return replacer.Replace(ns) + "srn"
return replacer.Replace(ns) + ".srn"
}
// LoadNS looks up the namespace specified by the given name `ns`
@ -149,6 +151,10 @@ func (r *Runtime) LoadNS(ns string) (*loadedForms, IError) {
for _, loadPath := range r.paths {
possibleFile := path.Join(loadPath, nsFile)
if r.debugMode {
fmt.Printf("[DEBUG] Looking for '%s' in '%s'", possibleFile, loadPath)
}
_, err := os.Stat(possibleFile)
if err != nil {

View File

@ -37,7 +37,6 @@ type Scope struct {
func (s *Scope) Lookup(rt *Runtime, k string) *Binding {
if rt.IsDebugMode() {
fmt.Println(s.parent)
fmt.Printf("[DEBUG] Looking up '%s'\n", k)
}

View File

@ -36,3 +36,13 @@ type IHashable interface {
func HashOf(in []byte) uint32 {
return crc32.Checksum(in, hashTable)
}
// CombineHashes combines two hashes and return a new one
func CombineHashes(hash1, hash2 uint32) uint32 {
// This way of composing hashes is used in libboost and almost everyone
// is using it. The 0x9e3779b9 is the integral part of the Golden Ratio's
// fractional part 0.61803398875… (sqrt(5)-1)/2, multiplied by 2^32.
// For more info: https://lkml.org/lkml/2016/4/29/838
hash1 ^= hash2 + 0x9e3779b9 + (hash1 << 6) + (hash2 >> 2)
return hash1
}