-
Notifications
You must be signed in to change notification settings - Fork 97
Chapter 8
Berry allows full freedom from the developer. But after some experience in coding with Berry, you will find that there are common mistakes that are hard to find and that the compiler could help you catch. The strict
mode does additional checks at compile time about some common mistakes.
This mode is enabled with import strict
or when running Berry with -s
option: berry -s
This is the most common mistake, a variable assigned without var
is either global if a global already exists or local otherwise. Strict mode rejects the assignment if there is no global with the same name.
No more allowed:
def f()
i = 0 # this is a local variable
var j = 0
end
syntax_error: stdin:2: strict: no global 'i', did you mean 'var i'?
But still works for globals:
g_i = 0
def f()
g_i = 1
end
Berry allows to override a builtin. This is however generally not desirable and a source of hard to find bugs.
map = 1
syntax_error: stdin:1: strict: redefinition of builtin 'map'
Berry tolerated multiple declaration of a local variable with the same name. This is now considered as an error (even without strict mode).
def f()
var a
var a # redefinition of a
end
syntax_error: stdin:3: redefinition of 'a'
In Berry you can declare local variables with the same name in inner scope. The variable in the inner scope hides the variable from outer scope for the duration of the scope.
The only exception is that variables starting with dot '.' can mask from outer scope. This is the case with hidden local variable .it
when multiple for
are embedded.
def f()
var a # variable in outer scope
if a
var a # redefinition of a in inner scope
end
end
syntax_error: stdin:4: strict: redefinition of 'a' from outer scope
Virtual members allows you to dynamically and programmatically add members and methods to classes and modules. You are no more limited to the members declared at creation time.
This feature is inspired from Python's __getattr__()
/ __setattr__()
.
The motivation comes from LVGL integration to Berry in Tasmota. The integration needs hundreds of constants in a module and thousands of methods mapped to C functions. Statically creation of attributes and methods does work but consumes a significant amount of code space.
This proposed features allows to create two methods:
Berry method | Description |
---|---|
member |
(name:string) -> any Should return the value of the specified name
|
setmember |
(name:string, value:any) -> nil Should store the value to the virtual member with the specified name
|
When the following code a.b
is executed, the Berry VM does the following:
- Get the object named
a
(local or global), raise an exception if it doesn't exist - Check if the object
a
is of typemodule
,instance
orclass
. Raise an exception otherwise - Check if object
a
has a member calledb
. If yes, return its value, if no proceed below - If object
a
is of typeclass
, raise an exception because virtual members do not work for static (class) methods - Check if object
a
has a member calledmember
and it is afunction
. If yes call it with parameter"b"
as string. If no, raise an exception
When the following code a.b = 0
(mutator) is executed, the Berry VM does the following:
- Get the object named
a
(local or global), raise an exception if it doesn't exist - Check if the object
a
is of typemodule
,instance
orclass
. Raise an exception otherwise- If
a
is of typeclass
, check if memberb
exists. If yes, change its value. If no, raise an exception. (virtual members don't work for classes or static methods) - If
a
is of typeinstance
, check if memberb
exists. If yes, change its value. If no, proceed below- Check if
a
has a member calledsetmember
. If yes call it, if no raise an exception
- Check if
- If
a
is of typemodule
. If the module is not read-only, create of change the value (setmember
is never called for a writable module). If the module is read-only, thensetmember
is called if it exists.
- If
To indicate that a member does not exist, member()
shall return nil
. Hence it is not possible to to have a virtual member with a value nil
.
You can also raise an exception in member()
but be aware that Berry might try to call methods like tostring()
that will land on your member()
method if they don't exist as static methods.
To indicate that a member is invalid, setmember()
should raise an exception.
Be aware that you may receive member names that are not valid Berry identifiers. The syntax a.("<->")
will call a.member("<->")
with a virtual member name that is not lexically valid, i.e. cannot be called in regular code, except by using indirect ways like introspect
or member()
.
Access to members of class object do not trigger virtual members. Hence it is not possible to have virtual static methods.
Modules do support reading static members with member()
.
When writing to a member, the behavior depends whether the module is writable (in memory) or read-only (in firmware).
If the module is writable, the new members are added directly to the module and setmember()
is never called.
If the module is read-only, then setmember()
is called whenever you try to change or create a member. It is then your responsibility to store the values in a separate object like a global.
Example:
class T
var a
def init()
self.a = 'a'
end
def member(name)
return "member "+name
end
def setmember(name, value)
print("Set '"+name+"': "+str(value))
end
end
t=T()
Now let's try it:
> t.a
'a'
> t.b
'member b'
> t.foo
'member foo'
> t.bar = 2
Set 'bar': 2
This works for modules too:
m = module()
m.a = 1
m.member = def (name)
return "member "+name
end
m.setmember(name, value)
print("Set '"+name+"': "+str(value))
end
Trying:
> m.a
1
> m.b
'member b'
> m.c = 3 # the allocation is valid so `setmember()` is not called
> m.c
3
More advanced example:
> class A
var i
def member(n)
if n == 'ii' return self.i end
return nil # we make it explicit here, but this line is optional
end
def setmember(n, v)
if n == 'ii' self.i = v end
end
end
> a=A()
> a.i # returns nil
> a.ii # implicitly calls `a.member("ii")`
attribute_error: the 'A' object has no attribute 'ii'
stack traceback:
stdin:1: in function `main`
# returns an exception since the member is nil (considered is non-existant)
> a.ii = 42 # implicitly calls `a.setmember("ii", 42)`
> a.ii # implicitly calls `a.member("ii")` and returns `42`
42
> a.i # the concrete variable was changed too
42
This guide drives you through the different options of packaging code for reuse using Berry's import
directive.
When you use import <module> [as <name>]
, the following steps happen:
- There is a global cache of all modules already imported. If
<module>
was already imported,import
returns the value in cache already returned by the first call toimport
. No other actions are taken. -
import
searches for a module of name<module>
in the following order:
- in native modules embedded in the firmware at compile time
- in file system, starting with current directory, then iterating in all directories from
sys.path
: look for file<name>
, then<name>.bec
(compiled bytecode), then<name>.be
. IfBE_USE_SHARED_LIB
is enabled, it also looks for shared libraries like<name>.so
or<name>.dll
although this optional is generally not available on MCUs.
- The code loaded is executed. The code should finish with a
return
statement. The object returned is stored in the global cache and made available to caller (in local or global scope). - If the returned object is a
module
and if the module as ainit
member, then an extra step is taken. The function<module>.init(m)
is called passing as argument the module object itself. The value returned byinit()
replaces the value in the global cache. Note that theinit()
is called at most once during the firstimport
.
Here is a simple example of a module:
File demo_module.be
:
# simple module
# use `import demo_module`
demo_module = module("demo_module")
demo_module.foo = "bar"
demo_module.say_hello = def ()
print("Hello Berry!")
end
return demo_module # return the module as the output of import
Example of use:
> import demo_module
> demo_module
<module: demo_module>
> demo_module.say_hello()
Hello Berry!
> demo_module.foo
'bar'
> demo_module.foo = "baz" # the module is writable, although this is highly discouraged
> demo_module.foo
'baz'
The problem of using modules is that they don't have instance variables to keep track of data. They are essentially designed for state-less libraries.
Below you will find an elegant way of packaging a class singleton returned as an import statement
.
To do this, we use different tricks. First we declare the class for the singleton as an inner class of a function, this prevents from polluting the global namespace with this class. I.e. the class will not be accessible by other code.
Second we declare a module init()
function that creates the class, creates the instance and returns it.
By this scheme, import <module>
actually returns an instance of a hidden class.
Example of demo_monad.be
:
# simple monad
# use `import demo_monad`
demo_monad = module("demo_monad")
# the module has a single member `init()` and delegates everything to the inner class
demo_monad.init = def (m)
# inncer class
class my_monad
var i
def init()
self.i = 0
end
def say_hello()
print("Hello Berry!")
end
end
# return a single instance for this class
return my_monad()
end
return demo_monad # return the module as the output of import, which is eventually replaced by the return value of 'init()'
Example:
> import demo_monad
> demo_monad
<instance: my_monad()> # it's an instance not a module
> demo_monad.say_hello()
Hello Berry!
> demo_monad.i = 42 # you can use it like any instance
> demo_monad.i
42
> demo_monad.j = 0 # there is strong member checking compared to modules
attribute_error: class 'my_monad' cannot assign to attribute 'j'
stack traceback:
stdin:1: in function `main`