This repository was archived by the owner on Sep 27, 2023. It is now read-only.
- Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathImplementation
188 lines (159 loc) · 8.5 KB
/
Implementation
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
A Guide to the Curv Implementation
==================================
Top level subdirectories in the repo:
cmake -- build scripts
curv -- source code for the 'curv' command line tool and REPL
docs -- documentation, intended for users of Curv
examples -- Curv programs (*.curv)
extern -- source code for 3rd party packages
ideas -- Brainstorming notes, not intended to be coherent, accurate,
or comprehensible to anyone other than the author.
lib -- text files to be installed in /usr/local/lib
libcurv -- C++ library source code: the core of the Curv implementation
tests -- unit tests
Directories created by the build system:
debug -- the debug build (slower, added runtime checks)
release -- the release build (faster)
There are 3 libraries in the libcurv directory:
libcurv -- Compiler, interpreter, language runtime.
Only depends on boost and double-conversion.
libcurv/viewer -- Graphics window for rendering shapes using GPU.
Depends on libcurv, OpenGL, Glad, GLFW.
libcurv/io -- Import and export various file formats.
Depends on libcurv, viewer, and file-format libraries.
LibCurv
-------
This is the biggest and most complex block of code.
All names are in the curv:: namespace.
No Global Variables
~~~~~~~~~~~~~~~~~~~
There are no mutable global variables in libcurv.
Any state needed by a function must be passed in as an argument.
This leads to a distinctive coding style, inspired by functional programming.
The console output stream is not a global variable. Instead, there is a
System object that contains global state needed at any time (compile time
or run time), such as the console (where we write error messages).
The following types are used to represent "global" state associated with
a particular phase of program translation or execution. Each of these contains
a System reference. A reference to one of these objects is passed to each
function associated with that phase.
Scanner -- parsing
Environ -- semantic analysis
Frame -- evaluation
SC_Frame -- SubCurv compiler
Pervasive Types
~~~~~~~~~~~~~~~
Shared<T> is a reference counted smart pointer to an object of type T.
It is faster and more compact than std::shared_ptr<T> in the C++ standard.
The limitation is that Shared<T> is not thread safe.
make<T>(constructor-arguments) allocates an object of type T and returns
a Shared<T>. class T is a subclass of Shared_Base.
All dynamically allocated objects are referenced using Shared<T> smart
pointers. They are reference counted, which means memory is freed automatically
when the object is no longer being referenced. Because reference counting
fails if the object graph contains cycles, we ensure that all objects in the
Curv language runtime are acyclic. Very few programming languages can
guarantee that all objects are acyclic, but Curv is designed for this.
Shared<const String> is the representation of character strings in
the Curv language runtime (see: Value). It is a UTF-8 format string.
stringify(arg1,arg2,...) converts a list of arguments of arbitrary type
to a Shared<const String>.
`const String_Ref&` is used to declare a function parameter which is a
character string. Valid arguments include a C++ string literal like "foo",
a C++ std::string, and a Shared<const String>.
`throw Exception(const& Context, const String_Ref& message)`
is the syntax for throwing an exception. This is used for reporting all
compile time and run time errors. When an error is printed, it includes a
stack trace: the stack trace information is in the Context object.
Any function that can throw an error will either have a 'const& Context'
argument, or it will have another argument from which a Context can be
constructed (the aforementioned Scanner, Environ, Frame and SC_Frame types).
Values
~~~~~~
The Curv language has 6 fundamental data types: Number, Symbol, String,
List, Record and Function. The mapping between these abstract types and
libcurv types is not 1-1. For example, in Curv, there is no fundamental
boolean type: instead, the symbols #true and #false are used to represent
boolean values. In libcurv, the symbols #true and #false are represented
specially as type 'bool', and non-boolean symbol values are represented as
type Symbol_Ref.
All values in the Curv language runtime are represented by type Value,
which is always passed by value. A Value occupies 64 bits. Numbers and booleans
are stored directly. Other types (Symbol, String, List, Record, Function, etc)
are stored as a pointer to a dynamically allocated object of type Ref_Value,
which has a reference count.
Here's how to convert between type Value and type bool:
Value{a_bool}
Convert a bool to a Value.
value.is_bool()
Returns bool: true if the value is a bool.
value.to_bool(const Context& cx)
Convert value to a bool, throw an error if it isn't a bool.
The 'cx' argument is needed for throwing an exception.
Here's how to convert between type Value and type Shared<const String>:
Value{a_string}
Convert a string to a Value.
is_string(value)
Returns bool: true if value is a string.
maybe_string(value)
Returns Shared<const String>: nullptr if value is not a string.
value_to_string(value, const Context& cx)
Returns a non-null Shared<const String>, or throws an exception
if value is not a string.
Curv Programs
~~~~~~~~~~~~~
A Curv program is represented by a Source object, which contains the
program text (an array of characters) plus the filename (optional).
To compile a Curv program into executable form, then to evaluate it into a
Value, you can:
* construct a Program object from a Source and a System
* call program.compile()
* call program.eval()
Here's what Program::compile() does:
* The Source is converted to a sequence of tokens using a Scanner object.
* A recursive descent parser called `parse_program` reads tokens from a Scanner
and returns a Phrase, which is a parse tree.
* Conceptually, the Curv grammar is divided into a Surface Grammar and a Deep
Grammar. See docs/language/Grammar.rst. The Surface Grammar is simple,
and just explains how to parse source code into a tree, even if the tree
isn't a valid program. `parse_program` just uses the Surface Grammar.
* The parse tree then undergoes semantic analysis by the analyser, whose
main data structure is an Environ. So we call phrase->analyse(environ),
and that returns a Meaning.
A Meaning is the executable form of a compiled Curv phrase.
A Meaning is one of the following:
* A Metafunction, which is like a function that can only be called at
compile time: it takes a Phrase as an argument and returns a Meaning as
a result. Kind of like a Lisp macro, except there are no user-defined
metafunctions at this time.
* An Operation, which represents run-time executable code.
An Operation is either an expression or a statement (but these are not
represented by C++ classes). A statement is a value generator, a field
generator, or an action.
The principal data structure passed around at runtime is a Frame&, which is
a stack frame. In other language runtimes, this would be a global variable.
If an Operation op happens to represent an expression, then op->eval(frame)
will evaluate the expression and return a Value, otherwise it will throw an
Exception which is printed as a run-time error message with a stack trace.
Compiling a shape value into a GPU program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the REPL, if an expression returns a Shape value, then instead of printing
the value in the normal way, we display the shape graphically in a Viewer
window.
We recognize a shape as a record with 5 required fields: dist, colour, bbox,
is_2d, is_3d. If we evaluated the expression using the Program class, then
we can use this Program object to construct a Shape_Program. Then we
call shape_program.recognize(value, render_opts), and it returns true
if the value is a shape. Note, render_opts is a distillation of command line
options that determine how a shape is to be rendered by a Viewer.
The render_opts object is updated if the shape contains the optional 'render'
field, which also controls rendering.
If shape_program.recognize() returns true, then we can construct a Viewed_Shape
using the Shape_Program and the Render_Opts. This does a lot of work, creating
a GPU program for execution by the Viewer.
<describe GPU compilation process here>
Viewer
------
curv::viewer::Viewer is the class that takes a compiled GPU program (a
Viewed_Shape object) and displays a 2D or 3D rendered shape in an OpenGL
graphics window.