abs-tudelft / tydi Goto Github PK
View Code? Open in Web Editor NEWTydi: an open specification for complex data structures over hardware streams
Home Page: https://abs-tudelft.github.io/tydi
License: Apache License 2.0
Tydi: an open specification for complex data structures over hardware streams
Home Page: https://abs-tudelft.github.io/tydi
License: Apache License 2.0
https://abs-tudelft.github.io/tydi/specification/physical.html#signal-omission
Specifies that:
stai
is contingent on C≥6∧N>1endi
is contingent on (C≥5∨D≥1)∧N>1strb
is contingent on C≥7∨D≥1As a result of these requirements, a Stream with C<5, D=0 and element lanes N>1 should not have an endi
signal.
This means that all lanes in a transfer must be valid, as there is no way to set individual lanes as being inactive.
The current specification implies that any transfer with C<5, D=0, N>1 must consist of Q elements where Q mod N = 0
.
It is not clear whether this is intentional, I can think of two other ways in which to make behavior consistent:
endi
is solely contingent on N>1 - i.e., it is always allowed to transfer multiple elements, and endi
can be used to encode lane validity when sequences do not align to the number of element lanes.While https://abs-tudelft.github.io/tydi/specification/physical.html#data-signal-description indicates that stai
and endi
are redundant when C≥7 (which adds support for a per-lane strb
(strobe) signal), it is not clear whether stai
and endi
are significant when strb
is low, although this is implied.
At lower complexities (C<7) the strb
signal still exists, but only as a single bit to indicate that the entire transfer's data is inactive.
For a concrete example, it is unclear what values stai
and endi
should have when transferring an "empty sequence".
stai
and endi
are insignificant when the strb
of that index's data lane is driven low. (Which at C < 7, means that stai
and endi
are insignificant altogether when strb='0'
.)
Another potential issue might arise from a lack of clarity around stai
and endi
when C≥7. While the above data
signal specification indicates that
while a source that desires individual control over the lanes and thus has C≥7 would probably always drive
endi
to N−1 andstai
to 0, a sink with complexity C≥7 still needs to accept input from sources that do useendi
andstai
.
This does not clarify whether a source with C≥7 should employ endi
and stai
in such a way.
In the event that a source transfers
stai: 0, endi: 0, strb: 11010
do the stai
and endi
signals take precedence? (I.e., only lane 0 is truly active.)
Now, what about:
stai: 0, endi: 0, strb: 01010
Since lane 0 is now inactive, taking my suggested fix, stai
and endi
are insignificant and lanes 1 and 3 are active.
Hence, I propose specifying that
"stai
and endi
are only significant when all strb
bits are driven high"
As this still allows for sources with C<7 to be compatible with sinks with C≥7, but does not allow for sources with C≥7 to create confusing transfers.
Here are some suggestions to improve the API for logical streams:
/// Rename from LogicalStreamType
pub enum LogicalType {
Null,
Bits(Positive),
Group(Group),
Union(Union),
Stream(Stream),
}
impl LogicalType {
/// Returns an iterator over resulting split items.
pub fn split(&self) -> Split { ... }
/// Returns an iterator over the physical streams resulting from splitting
/// and mapping the element streams to physical streams.
pub fn physical(&self) -> PhysicalSplit { ... }
}
/// An iterator over split items from a LogicalType.
pub struct Split(indexmap::set::IntoIter<SplitItem>);
impl Iterator for Split {
type Item = SplitItem;
}
impl DoubleEndedIterator for Split { ... }
impl ExactSizeIterator for Split { ... }
/// An iterator over physical split items from a LogicalType.
pub struct PhysicalSplit(indexmap::set::IntoIter<SplitItem>);
impl Iterator for PhysicalSplit {
type Item = PhysicalSplitItem;
}
impl DoubleEndedIterator for PhysicalSplit { ... }
impl ExactSizeIterator for PhysicalSplit { ... }
/// An element stream with a path name and LogicalType. Contains no nested
/// streams.
pub struct ElementStream {
path_name: PathName,
logical_type: LogicalType,
}
impl ElementStream {
/// Returns the LogicalType of this element. Contains no nested streams.
pub fn logical_type(&self) -> &LogicalType {
&self.logical_type
}
/// Return all fields in this element stream
pub fn fields(&self) -> Fields { ... }
}
impl From<ElementStream> for PhysicalStream { ... }
pub struct Signals(LogicalType);
impl Signals {
/// Returns the LogicalType of this element.
pub fn logical_type(&self) -> &LogicalType {
&self.0
}
/// Returns all fields in these async signals.
pub fn fields(&self) -> Fields { ... }
}
/// A split item is either an async signal (outside streamspace) or an element
/// stream (no nested streams).
pub enum SplitItem {
Signals(Signals),
Stream(ElementStream),
}
/// A split item is either an async signal (outside streamspace) or a physical
/// stream.
pub enum PhysicalSplitItem {
Signals(Signal),
Stream(PhysicalStream),
}
For the uses case of @ahadnagy this would result in something like this:
let haystack: LogicalType = ...;
let needle: &[Name] = ...;
let search: Option<SplitItem> = haystack
.split()
.find(|split_item|
match split_item {
SplitItem::Signals(signals) => signals.fields(),
SplitItem::Stream(element_stream) => element_stream.fields(),
}
.keys()
.windows(needle.len())
.any(|name| name == filter)
);
Given that we modify the Fields::keys
method to return &[Name]
instead of &PathName
.
This also solves #27. We could also use a marker trait to distinguish between nested and unnested LogicalType
s.
https://abs-tudelft.github.io/tydi/specification/physical.html#signals
Mentions that strb
encoding individual lane validity is contingent on C≥8. This is repeated in a few other places.
However, https://abs-tudelft.github.io/tydi/specification/physical.html#signal-omission makes strb
contingent on C≥7.
And https://abs-tudelft.github.io/tydi/specification/physical.html#complexity-c likewise suggests that "The indices of the active data lanes can be described with a simple range." only applies to C<7.
strb
encoding individual lane validity/activity should be contingent on C≥7. As otherwise, C=7 is identical in functionality to C=6.
Instead, the intent was most likely to have C≥7 add support for individual lane validity (a strb
bit per element lane), while C≥8 adds support for individual lane-based sequence terminations (a last
signal per element lane).
The current general flow of the generator tool got a bit messy and is as follows:
CLI Options
|
V
+------------- CLI
|
SDF files
|
V
Parser -> Tydi -> Physical -> Common (canon) -> Back-end -> Source files
| ^
| |
+---------------> Common (user) -------+
It is desired to change the flow to something where the common library is just a helper module for the various back-ends.
The entry point of a back-end is a Tydi generator project.
CLI Options
|
V
+----- CLI -------+
| |
SDF files Abstraction lvl.
| |
V v
Parser -> Tydi -> Back-end -> Source files
Currently splitting creates new LogicalStreamTypes, but they don't support nested streams, so they should be a different sorts LogicalStreamTypes.
We should consider a type-safe approach to prevent users from being able to make mistakes.
Originally posted by @mbrobbel in https://github.com/abs-tudelft/tydi/diffs/6
The following things have come up that are either broken or aren't very nice about the current specification.
Stream
nodes and to physical streams that specifies whether empty sequences are supported or not.Tuple(N, T)
may be used as a shorthand for Group(0: T, 1: T, ..., N-1: T)
.This is a tracking issue for the first steps towards implementing a hardware description language for Tydi types.
Currently the Tydi crate provides modules with the logical and physical stream type definitions and methods as described in the specification. There is also a design module that defines streamlets, a parser module for streamlet definition files and a generator module that can generate HDL templates based on streamlet definition files. The tydi
command line application exposes these capabilities.
In order to provide an ergonomic solution for #51, this issue tracks the requires steps to move from an (embedded) library based approach to a high-level language approach. This issue attempts to provide tasks and steps for the initial phase of the language, compiler and tooling design.
The benefits from adapting Tydi typed streams (🐬) in hardware designs may be snowed under the added effort and struggle (🐠) for hardware developers to make their designs compatible with Tydi streams.
The goals during this phase
for the Tydi language:
for the tooling:
During this phase the Tydi language does not support:
Tydi's syntax is heavily inspired by Rust's syntax.
.td
.# constant generic
const
# streamlet interface mode
in
out
# streamlet definition
streamlet
# type definition
type
# use statement
use
# type constructs
group
union
stream
# stream synchronization modes
sync
desync
flatten
flatdesync
# stream direction
forward
reverse
# boolean literals
true
false
( # open paren
) # close paren
[ # open square
] # close square
{ # open curly
} # close curly
< # open angle
> # close angle
: # colon
; # semicolon
:: # path separator
, # comma
= # equals
! # exclamation
# decimal number
[0-9]+
# hexadecimal number
0x[0-9a-fA-F]+
# identifier
[a-zA-Z][a-zA-Z_0-9]*
module = { item } ;
item = use_statement | type_definition | streamlet_definition ;
use_statement = "use" , identifier , { "::" , identifier } , ";" ;
type_definition = "type" , identifier , [ generic_definition ] , "=" , type , ";" ;
streamlet_definition = "streamlet" , identifier , [ generic_definition ] , (
"(" , interface , { "," , interface } , ")"
| "{" , streamlet_interface , { "," , streamlet_interface } , "}"
) ;
streamlet_interface = identifier , ":" , interface ;
interface = mode , type ;
mode = "in"
| "out" ;
type = stream
| union
| group
| path ;
stream = "stream!" , "(" , type , type , direction , synchronicity , number , ")" ;
direction = "forward"
| "reverse" ;
synchronicity = "sync"
| "desync"
| "flatten"
| "flatdesync" ;
union = "union" , ( "(" , variants , ")" | "{" , fields , "}" ) ;
group = "group" , ( "(" , variants , ")" | "{" , fields , "}" ) ;
path = identifier , [ generic_args ] ;
variants = type , { "," , type } ;
fields = identifier , ":", type , { "," , identifier , ":" , type } ;
generic_definition = "<" , generic_param , { "," , generic_param } , ">" ;
generic_args = "<" , generic_arg , { "," , generic_arg } , ">" ;
generic_arg = type
| "(" , expr , ")" ;
generic_param = [ "const" ] , identifier ;
expr = expr , "+" , expr
| expr , "*" , expr
| "(", expr , ")"
| identifier
| number ;
identifier = letter, { letter | digit | "_" } ;
hex = "0x" , hex_symbol , { hex_symbol } ;
hex_symbol = digit | ? [a-fA-F] ? ;
number = digit , { digit } ;
letter = ? [a-zA-Z] ?;
digit = ? [0-9] ?;
Documented example of Tydi source file using the proposed grammar:
// A line comment.
// A use statement to bring Tydi types in scope for use in this module.
// This is like Rust's use statement, without support for combining multiple
// leafs with a single root in a tree-like syntax.
use axi::AXI4;
use axi::lite::AXI4_Lite;
// A type definition statement to define a new `Bit` type. The right hand side
// uses the built-in constructor for the primitive `Bits` type. When using the
// std, these constructors can be replaced with its corresponding type
// definition, i.e. `std::Bits`.
// Please note, this is not a type alias, or synonym. The compiler considers
// `Bit` and `bits!(1)` two different types. They are however compatible
// according to the Tydi specification. This allows for typesafe wrappers for
// example for signed/unsigned integer types.
type Bit = bits!(1);
// Type definitions can be generic. This example shows a constant generic for
// a `Bits` type. This allows the usage of a const expression in other type
// definitions, as shown for example in the `Bytes` type definition.
type Bits<const N> = bits!(N);
type Byte = Bits<(8)>;
type Bytes<const N> = Bits<( 8 * N )>;
// For product and sum types the `group` and `union` keywords are used. Tydi
// supports both unnamed variants (`Foo`) and named fields (`Bar`) for both
// groups and unions.
type Foo = group(Bit, Bit);
type Bar = group {
foo: Bit,
bar: Bit
};
type FooBar = union(Foo, Bar);
type BarFoo = union {
bar: Bar,
foo: Foo
};
// For streams the built-in `stream!` constructor can be used. The arguments of
// this constructor are (`element_type`, `user_type`, `direction`,
// `synchronicity`, `dimensionality`). The keywords as listed above are valid
// on corresponding locations here. Please note that, like the `bits`
// constructor, the `stream` constructor is used most often in the std. With
// additional generic argument type support these constructors can be exposed
// as generic type definitions (const generics already allow for the `Bits`
// definition). In the future there may be a type definition in the std for
// `Stream<..?>`.
// Below are the some type defintions from the Tydi paper, which also shows how
// type definitions can be generic over types.
type Dim<T> = stream!(T, Null, forward, sync, 1);
type New<T> = stream!(T, Null, forward, sync, 0);
type Des<T> = stream!(T, Null, forward, desync, 0);
type Flat<T> = stream!(T, Null, forward, flatten, 0);
type Rev<T> = stream!(T, Null, reverse, sync, 0);
// Now using these type definitions, more type definitions can be written.
// These examples are also from the Tydi paper.
type List<T> = Dim<T>;
type Vec<T, const N> = group(Bits<(N)>, New<T>);
// Streamlet definitions define a streamlet with its interface types. Streamlet
// definitions can be generic and the interface names can be unnamed or named.
type Sum<const N> = Bits<(N)>;
type Carry = Bit;
streamlet HalfAdder<const N>(
in Bits<(N)>,
in Bits<(N)>,
out Sum<(N)>,
out Carry
)
streamlet Adder<const N> {
input: in group {
a: Bits<(N)>,
b: Bits<(N)>,
carry: Carry,
},
output: out group {
sum: Sum<(N)>,
carry: Carry
}
}
// Streamlets support stream types on their interfaces.
type Char = Byte;
type String = List<Char>;
streamlet WordCounter<const N, const M> {
words: in String,
counts: out List<
group {
words_counted: Bits<(N)>,
bytes_read: Bits<(M)>,
}
>,
}
// And one more example e.g. `sha.td`:
use std::List;
streamlet SHA256(in List<Bits<(512)>>, out Bits<(256)>)
The grammar listed here is a proposal and open for discussion. There are already several issues to be discussed:
LL(1)
parsingconst N: i32
todo
const N: ?
?The Tydi compiler takes Tydi source files, parses them into an abstract syntax tree, resolves names, checks types and generates an in-memory high-level intermediate representation (hir) that can be used by backends for compiler output. References to sources are tracked outside the hir to allow us to provide a library for construction of these structures for generation tools (similair to the current approach).
In the initial phase of the project (type and streamlet definitions only) the compiler output consists of purely structural HDL templates (and helper methods) or design visualizations.
A Tydi project consists of a manifest (tydi.toml
) with project meta information and source files with type and streamlet defintitions.
tydi.toml
is a project's manifest file that contains all metadata and configuration information about the project.
The Tydi manifest file is inspired by Rust's Cargo.toml.
This example tydi.toml
shows all valid fields of a project manifest.
[project] # Project metadata
name = "std" # The project name
authors = ["Delft University of Technology"] # List of project authors
description = "The Tydi standard library" # Optional description of the project
[dependencies] # Dependencies configuration
axi = { path = "/axi" } # Example of a path dependency
wishbone = { git = "[email protected]:...", ... } # Example of a git dependency
The files in a Tydi project are organised with the project manifest in the root directory of the project, and all module files in a src
directory. The target
directory is used for compiler output. This setup is inspired by Rust's cargo
behavior.
> project_name
> tydi.toml # project file
> src # source directory
> lib.td # root module file (project_name)
> flow # flow module directory
> mod.td # flow root module file (project_name::flow)
> a.td # a module file part of the flow module (project_name::flow::a)
> b.td # b module file (project_name::b)
> target # output directory
> deps # dependency cache
> ...
// tydi std
// primitive (built-in)
type Bits<const N> = bits!(N);
// stream primitives
type Dim<T> = stream!(T, Null, forward, sync, 1);
type New<T> = stream!(T, Null, forward, sync, 0);
type Des<T> = stream!(T, Null, forward, desync, 0);
type Flat<T> = stream!(T, Null, forward, flatten, 0);
type Rev<T> = stream!(T, Null, reverse, sync, 0);
// extended primitives
type Null = Bits<(0)>;
type Bit = Bits<(1)>;
type Byte = Bits<(8)>;
type Bytes<const N> = Bits<( 8 * N )>;
// integers
type int<const N> = Bits<(N)>;
type uint<const N> = Bits<(N)>;
// containers
type List<T> = Dim<T>;
type Vec<T, const N> = group(Bits<(N)>, New<T>);
// todo: char encoding generic?
type String = List<Byte>;
log2!
tydi
cliimpl Adder { ... }
type Foo = Bit; type Bar = Bit;
-> Foo as Bar
The lexical-core package can't be compiled due to the standard library defining the ::BITS constant: rust-lang/rust#81654
E.g.:
error[E0308]: mismatched types
--> [...]/lexical-core-0.6.7/src/atof/algorithm/math.rs:1049:42
|
1049 | let mut count = index.saturating_mul(Limb::BITS);
| ^^^^^^^^^^ expected `usize`, found `u32`
This prevents Tydi from being compiled, but can be resolved by updating the lexical-core package (performing an update of all packages does not appear to have an adverse effect, either).
When a Stream contains another Stream as its data
, the Split function assigns both the parent and child streams "∅" (empty name), and employs "flattening" to combine their throughput
, synchronicity
, dimensionality
and direction
.
When a Stream has no element-manipulating data (data
is either Null or a Stream) and no user
property, it is discarded from the result. In effect, this creates a new physical stream with the original child Stream's data
, combined with of the parent Stream's properties.
When keep
(x) is true and/or user
(T_u) is non-Null, the parent Stream must be retained.
If a parent Stream has keep
=true and/or a non-Null user
property, both Streams are still assigned "∅", but the parent Stream will conflict with the child Stream. Implementing the result of the Split function as a map in code, this means that either the child Stream simply replaces the parent Stream altogether (thereby losing the parent Stream's user
property), or the Split function fails.
However, this behavior is not described in the specification, it only specifies that the names resulting from the Split function "are case-insensitively unique, emptyable strings consisting of letters, numbers, and/or underscores, not starting or ending in an underscore, and not starting with a digit" (emphasis mine).
Right now, on an implementation level: The Split function should fail when it encounters a situation where two physical Streams have identical names.
On a specification level: It should be illegal for nested Streams (Streams which only have another Stream type as their data
) to have a keep
and/or user
property on more than one of these Streams, and "flattening" should incorporate the singular user
property into the resulting physical stream.
Alternatively, Streams should have a non-empty name
property, as this will avoid conflicts in the result of the Split function.
For the specification: CC-BY
For the specification: ?
For the implementation: Apache-2.0
README.md
about licenses for this projecthttps://abs-tudelft.github.io/tydi/specification/physical.html#last-signal-description
[C<4] It is illegal to assert a
last
bit for dimension j without also asserting thelast
bits for dimensions j′<j in the same lane.[C<4] It is illegal to assert the last bit for dimension 0 when the respective data lane is inactive, except for empty sequences.
The first rule suggests that at C<4, and (as an example) D=3, it is not possible to assert last="100"
or last="110"
, or last="010"
).
This means that at C<4, it is illegal to transfer empty outer lists, which in turn means that Stream complexity has an effect on the kinds of data transferred.
For instance, the example given in the last signal description for C≥8, D=2
["Hello", "World"], ["Tydi", "is", "nice"], [""], []
makes use of an empty outer list (the last element, []
). This requires asserting last="10"
.
It is not clear whether this is intentional. However, I am of the opinion that Stream properties should not affect the kind of data which can be transferred.
As such, I recommend amending the first rule in the Background section of this issue to also include an exception for empty sequences.
Using https://abs-tudelft.github.io/tydi/specification/physical.html#signals
For N lanes, indexed 0 through N-1:
C < 8: All last
bits for lanes 0 to N−2 inclusive must be driven low by the source, and may be ignored by the sink.
C < 8: All strb
bits must be driven to the same value by the source. The sink only needs to interpret one of the bits. (strb
effectively doesn't exist, except to indicate empty sequences)
C < 6: stai
must always be driven to 0 by the source, and may be ignored by the sink. (Elements are aligned to lane 0.)
C < 5: endi
must be driven to N−1 by the source when last
is zero, and may be ignored by the sink in this case. (Effectively, all lanes must be used, except when transferring the end of a sequence.)
C < 4: It is illegal to assert the last
bit for dimension 0 when the respective data lane is inactive, except for empty sequences. (Effectively, last
may not be postponed.)
Taking a sequence of 7 elements with dimensionality D = 1 over a physical stream with 4 element lanes and complexity C < 4:
Transfer 1:
[active, active, active, active
All lanes must be used, because stai must be 0, and endi must be N-1. (Because C < 5 and C < 6)
Transfer 2:
active, active, active], inactive
?
The data
must be aligned to lane 0 (C < 6: stai must be 0)
The last
of dimension 0 must be asserted on the third lane. (C < 4: It is illegal to assert the last bit for dimension 0 when the data
of that lane is inactive.)
But also, the last of any dimension must actually be asserted on the fourth lane? (C < 8: last of lanes 0 through N-2 must be driven low. strb effectively doesn’t exist.)
The key inconsistency is derived from:
C < 4 It is illegal to assert the
last
bit for dimension 0 when the respective data lane is inactive, except for empty sequences.
At C < 8, the last
signal does not refer to specific data lanes, but operates on a per-transfer level. The intent of this rule is to prevent postponing the last
signal, not to establish requirements for data lanes with respect to the last
signal. Hence, the rule should probably be changed to say:
C < 4 It is illegal to assert the
last
bit for dimension 0 when the transfer data is inactive, except for empty sequences.
or
C < 4 It is illegal to assert any last bit when the transfer data is inactive, except for empty sequences.
Currently, it is allowed to specify a reverse signal on the Stream node.
It would be nice to also allow reverse signals on signals that are not streams.
It seems more logical to me that reverse should be a property of a group and union field, as "reverse" is always relative to something else.
Since at the type level, interface modes are (obviously) not exposed, it must be relative to something else within a Logical"Stream"Type, and thus it can only be another group or union field.
If such a group or union element with a reversed field is streamed, the "synthesis" algorithms probably need to be adjusted to create new streams for those specific fields.
Example of a Streamlet Definition File with:
Streamlet dolphin {
x : out Stream<Group<a: rev Bits<1>, b: Bits<1>>>
}
Would result in two streams: x.a going out and x.b coming in, both having the same properties of the parent stream.
For unions, if the first field is reversed, e.g.:
Streamlet triggerfish {
y : out Stream<Union<a: rev Bits<1>, b: Bits<1>>>
}
Not entirely sure but I think this should result in two streams:
y.(tag & b) going out and may not assume y.a. will send something before sending the tag.
y.a coming in.
Currently, the spec says:
https://abs-tudelft.github.io/tydi/specification/logical.html#union-semantics
The "tag" field is used to represent which variant is being represented, by means of a zero-based index encoded as an unsigned binary number.
Do we want to allow custom tag enumeration of union variants?
If yes, we should add this to the spec.
If no, we should make explicit that the current text refers to the field index in the union declaration.
In the example, the tag enumeration is done by order of variant appearance in the union declaration, which seems most logical and straightforward, and it is similar to what most programming languages do as far as I am aware.
Currently, Streamlet Definition Files support only the declaration of multiple streamlets as such:
Streamlet foo (
x : in Stream<Bits<2>>,
y : out Stream<Bits<2>>
)
Streamlet bar ...
In the internals of the generator tooling, one file is parsed to one library, containing the declared streamlets.
It would be nice to be able to define types, and use them across multiple libraries that reside in a single project.
Since the files do not only contain streamlet declarations, it would be nice to rename the files to .tydi file, and then be able to type something like the following.
For example, in some file: mills.tydi
type Wheat = Stream<Bits<1>>;
type Flour = Stream<Bits<4>>;
Streamlet windmill (
wheat : in Wheat,
flour : out Flour
)
And another file: bakery.tydi
use mills::Flour;
type Cookie = Stream<Bits<1>>;
Streamlet bakery (
flour : in Flour,
cookie : out Cookie
)
This means the type of the interfaces windmill.flour
and bakery.flour
are equal (their in/out mode is not part of the type), and can be connected if their mode corresponds accordingly.
However windmill.wheat
and bakery.cookie
do not have the same types. In other words, the windmill outputs the exact same type of Flour that the bakery can use as input, but Wheat is not a Cookie.
The (current) decision is to use a single underscore for hierarchy and make underscores illegal in field identifiers. The spec needs to be updated to reflect this at some point.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.