1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
|
╒═════════════════════════════════════════════════════════════════════════════╕
│ THIS PROJECT IS NEW and the code is UNFINISHED. DON'T USE IT │
╘═════════════════════════════════════════════════════════════════════════════╛
LittleScript is a BASIC-like lightweight scripting language for
microcontrollers. It's meant for adding scriptability to your firmware, not
for writing your entire code in. LS has some restrictions to make it more
embedded-friendly:
- Identifiers capped at 6 characters
- Integer arithmetic by default
- No garbage collection (strings work like C strings)
- Allocation from a pool of fixed blocks
- Code is not stored all at once in RAM - can execute in place from slow
external devices with some caching, etc.
┌─────────────────┐
│ Copying and use │
└─────────────────┘
This software disclaims copyright. Do what you want with it. Be gay, do crime.
Originally written by Alexis Lockwood in 2021. Ⓐ
┌───────────────────────────┐
│ What's implemented so far │
└───────────────────────────┘
- Basic high-level API
- Main lex-parse loop
- Expression evaluator
- Variable storage
- LET, PRINT, GOTO, GOSUB, RETURN keywords
┌───────────┐
│ Todo list │
└───────────┘
- Other keywords
- Non-integer variable types
- String will always be supported
- Floating point as optional compile-in
- Better testing
- Compile-time options for:
- Internal asserts for better LS_INTERNAL_ERROR info
- Hardwiring the context to a static, this should reduce code size a lot
- Disabling features not needed, including
- Hooks
- Label cache
- Fetcher (always execute in RAM)
- OPER_* should be LS_OPER_*
- Get rid of all this _ctx_ shit, it's a weird habit, they're just objects
╒════════════════════════╕
│ THE SCRIPTING LANGUAGE │
╘════════════════════════╛
┌───────────┐
│ Positions │
└───────────┘
Unlike BASIC which uses line numbers, LittleScript uses a system of named
labels and numbered small directional jumps. These are inspired by GNU
Assembler jump targets:
REM examples/positions.bas
GOSUB hello
PRINT "Returned"
END
hello: PRINT "Hello, world"
GOTO +1
PRINT "This line is skipped"
1: RETURN
There is no requirement of declaration order. If the target of a GOSUB, GOTO,
or any other line number argument is an identifier that has not been seen yet,
the interpreter will scan forward until it finds it.
When a number is used, it must be prefixed with + or -, and refers to the next
line forward or backward matching that number.
┌───────┐
│ Types │
└───────┘
LittleScript currently has no floating or fixed point support; all numbers are
integers and this is the default type. As with most BASIC variants, the $
suffix can be used for string variables:
S$ = "Hello, world"
PRINT S$
Strings are interpreted as ASCII and can contain all byte values including 0.
This allows them to be used to contain arbitrary binary data. Internally,
they are represented as a list of eight-character chunks.
Lists are single-dimensional only, and are implemented using linked lists
rather than arrays. No DIM is necessary to set them up since new elements can
always be linked in from the object pool, but DIM may still be used to fail
early if memory is limited. To optimize sequential access, a pointer to the
last element accessed is stored in the array object, thus re-accessing the
last element or the next element is O(1) and iterating over the sequence is
O(n).
┌─────────────┐
│ Identifiers │
└─────────────┘
Identifiers are eight characters long (same as a single chunk of a string).
An identifier must be created (via LET or another assignment keyword) before
their values can be accessed - they have no default. With no suffix an
identifier has integer type. With a $ suffix, it is a string, and with a
() suffix, it is a list.
┌───────────┐
│ Minimizer │
└───────────┘
Because small embedded arguments are the target, a minimizer is provided that
condenses a script to a smaller form. In the smallest setting, keywords are
replaced by single-byte abbreviations - these are listed in the keyword
documentation.
┌───────────┐
│ Functions │
└───────────┘
Littlescript supports true functions as an extention to the GOSUB syntax.
Arguments are given in parentheses in key=value format, and a new scope is
created in the called function with these values. The AS keyword can create
a return value:
REM examples/add.bas
a = 2
b = 3
GOSUB Add(a = a, b = b) AS c
PRINT "a + b = ", c
END
Add: 'Add(a, b) => a + b
RETURN a + b
Because a new scope is created, values are passed by value: if the function
modifies them, the modification only occurs in the new scope. To pass by
value, name the variables accordingly and omit from the GOSUB statement; they
will be located and modified in the next scope up.
┌─────────────────────┐
│ Variables and scope │
└─────────────────────┘
LittleScript has both global and local variables, differentiated by the case
of the first character (uppercase = global, lowercase or not a letter = local).
A new local frame is pushed on GOSUB, and deleted on RETURN.
┌──────────────────┐
│ Native functions │
└──────────────────┘
(TODO)
┌───────┐
│ Files │
└───────┘
Many LittleScript systems will not have a traditional file storage, but may
want a way to read and write streams. The OPEN, CLOSE, READ, and WRITE keywords
defer to callbacks provided to LittleScript by the implementing system. The
following syntax is proposed for stream descriptors:
COMn [speed] [parity] [data]
FILE device /path/to/file
┌────────────┐
│ Statements │
└────────────┘
KEYWORD DOES
ABS(n)
Return the absolute value of n
ASC(s$[, n])
Return the byte value of the first or nth character
of s$
CALL fun(...)
Call native function `fun` with arguments ...
See NATIVE FUNCTIONS for the calling convention. `fun`
must be a literal.
CAT s$ t$
Concatenate string t$ onto the end of s$.
CHR s$ n
Append the byte value n to the end of string s$.
CLOSE [#n[,#n]...]
Close file numbers (or all files, if none specified).
File (or stream) access is provided by the system
implementing LittleScript and may not be available.
DATA expr[ expr...]
Data for READ
DEF FN name[(args...)] expr
Create a function with the given name.
END
Stop execution and return to caller
ERASE ident [ident ...]
Deallocate any space held by ident and remove that identifier.
ident may point at a list, a string, a function defined by
DEF FN, or any other variable. Note that because LittleScript
is not garbage collected unlike BASIC, you must ERASE anything
you create if you do not want to leak memory (with the
exception that everything created in a stack frame is erased
when that stack frame is popped).
FN name[(args...)]
Execute the function specified by `name`, returning its value.
FOR ident = start TO end [STEP n]
NEXT [ident]
Iterate ident from start to end by n (default=1).
When END is encountered, return back if the
terminating conditions have not been met. If ident
is not specified for END, walk outward from the inner
loop, repeating on the first one that has not
terminated.
Start and end values are restricted to signed
16 bit.
FREE ident[ ident ...]
Deallocate any space held by ident and remove that
identifier. Note that LittleScript is not garbage
collected - if you're done with a string, free it.
GOSUB label[(arg[, arg, ...])] [AS ident]
Push a stack frame and jump to the label. On return,
if a value was returned, store it in ident.
If args are specified, they are added to the function's local
scope.
GOTO label
Go directly to the label
HEX s$ n
OCT s$ n
DEC s$ n
Append the hexadecimal, octal, or decimal representation of n
to s$.
IF cond THEN ...
IF cond GOTO ...
If cond is true, perform the action.
cond is an expression, and is true if nonzero.
INPUT #n [AT ?] [UNTIL b] [COUNT n], ident [, ident, ...]
Read from a stream into variables. Generally only
string variables are supported, but this is
implementation-defined. Stop at either the byte
value b (if UNTIL specified) or the byte count n
(if COUNT specified), whichever comes first.
AT is interpreted as for WRITE.
LEN(s$)
Return the length of the string
LET ident [=] val
Assign the value of a variable. This is the default
keyword; statements lacking a keyword are parsed
as LET.
OPEN s$ AS #n
Open a file/stream. The "path" is more likely to be
some form of stream descriptor (see FILES), and may
be a literal or a string variable.
PACK n[, n...] AS s$ [: fmt]
UNPACK n[, n...] FROM s$ [: fmt]
Pack or unpack the provided values in binary format
into/from a string, with specific format optionally
specified. This can be used to assemble arbitrary
binary data. See PACK AND UNPACK.
PRINT [TO #n] val [... val...] [,]
Print values, to a stream or to standard output.
If the comma is provided, omit newline.
RANDOMIZE [seed]
Initialize the PRNG. If seed is not specified, it will be
initialized with the system's preferred random seed source.
Not all systems guarantee good quality seeds.
READ ident[ ident...]
Read values from DATA statements into the identifiers
given. Because LittleScript doesn't always have
access to the entire program at once like BASIC does,
it works a little differently. By default, the read
pointer is uninitialized; on the first READ, the
interpreter will search *forward* for the first DATA
statement it finds. Once it *executes* that DATA
statement, the read pointer is reset back to the
uninitialized state. Every stack context has its own
read pointer.
REM
Comment. Comments may also start with ', and ' may
appear anywhere in a line.
RESTORE [label]
Reset the read pointer. If label is specified, set it
to the next DATA statement after that label.
Otherwise, set it to the uninitialized state.
RETURN [expr]
Pop a stack frame. If the previous GOSUB specified a return
location using AS, return expr into it.
RND
Return a random number from 0 to 65535.
SWAP m, n
Swap the contents of two variables or list indices
VAL(s$)
Returns the numerical value of a string
WHILE cond
WEND
Loop as long as cond is true (nonzero).
WRITE #n [AT ?] val [val ...]
Write values to a stream. The interpretation of types
depends on the stream implementation. If AT is
specified, this implementation-defined value is also
passed to the implementation (it is usually an
address, for interfaces like I2C).
┌──────────┐
│ Keywords │
└──────────┘
Following is the full list of keywords, and the single-byte abbreviations that
may be used in their place.
(TODO: update the abbrev values)
ABBREV KEYWORD
80 ABS
81 AS
82 ASC
83 AT
84 ATN (not implemented, reserved)
85 CALL
86 CAT
87 CHR
88 CLOSE
89 COS (not implemented, reserved)
8A COUNT
8B DATA
DEF
8C END
ERASE
8D ERROR
FN
8E FOR
91 GOSUB
92 GOTO
93 IF
94 INPUT
95 LEFT
96 LET
97 LOG (not implemented, reserved)
98 MID
99 NEXT
9A ON
9B OPEN
9C PACK
9E PRINT
A0 RANDOMIZE
A1 READ
A2 REM
A3 RESTORE
A4 RETURN
A5 RIGHT
A6 RND
A7 SIN (not implemented, reserved)
A8 SQR (not implemented, reserved)
A9 STEP
AA SWAP
AB TAN (not implemented, reserved)
AC THEN
AD TO
AE UNPACK
AF UNTIL
B0 VAL
B1 WEND
B2 WHILE
B3 WRITE
|