Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save SeijiEmery/4e9d4fc9b21fc0dc26e1f0932095b4e8 to your computer and use it in GitHub Desktop.
Save SeijiEmery/4e9d4fc9b21fc0dc26e1f0932095b4e8 to your computer and use it in GitHub Desktop.
a few language ideas:
A few language design ideas, mostly focused on safety + ease of use for mid-to-large sized projects:
1. New (actually old) method syntax; treat methods as procedures with complex argument semantics, NOT functions (which they generally aren't).
C Syntax:
Result foo (Arg1 arg1, Arg2 arg2, Arg3 arg3) {
int local1 = ...;
double local2 = ...;
}
New syntax:
method foo {
in Arg1 arg1
in Arg2 arg2
in Arg3 arg3
out Result result
local int
local double
local1 = ...
local2 = ...
out = ...
}
The local ... declarations can be replaced with let + type inference:
let local1 = ...
let local2 = ...
The out declaration can be replaced with a return statement + type inference:
return ...
Alternatively, the in / out / local values can have initializers, and be declared in any order:
method foo {
in Arg1 arg1 = ...
in Arg2 arg2 = ...
local int local1 = ...
local int local2 = ...
in Arg3 arg3
out Result result = ...
}
All types can be replaced by auto (if type can be inferenced) / typeof (explicit; dependency on outside value):
method foo {
in auto arg1 = 10
in typeof(arg1) arg2 = 12
local auto local1 = arg1 + arg2
local auto local2 = arg2 ** 10
in typeof("Hello") arg3
out auto result = "${arg3}: ${arg1}, ${arg2}"
}
Semantically, we follow the following rules:
– in variables are declared in order of appearance
– out variables can be implicitely defined via return (either as single variable or tuple)
– uses UFCS; the 1st variable is equivalent to 'this'
– the first variable is aliased as '@', and fields can be accessed as '@x' => 'this.x'
Advantages:
– more flexible, clearer semantics; we can have the following storage specifiers:
– in, inout, out
– const (modifies other specifiers)
– local (creates local variable)
– global (references global variable)
– local global (sounds non-sensical, but equivalent to static function-local in C)
– threadlocal
– threadshared
– etc
– more than one return value; values can be bound individually by name
– unified declaration syntax (re-used when defining structs)
– method signatures are cleaner for large methods
– input variables _could_ be declared in any order (though that might be confusing...)
– could autogenerate interface declarations FWIW
– autogenerating documentation easier b/c simpler syntax
– syntax much simpler than C and easier to parse; only need to track which context you're in
(supports nested methods)
2. For function calls, all variables can be bound using names a la python / objc.
Equivalent:
let result = foo(1, 2, 3)
let result = foo(arg1 = 1, arg2 = 2, arg3 = 3)
foo(arg1 = 1, arg2 = 2, arg3 = 3, result = &myvar)
3. Functional-style data / methods, not object-oriented objects / methods.
All methods declared outside data structures, and called using OO syntax using UFCS.
Thus, values can be mixed-in more easily, etc.
"Class" declaration:
type Duck {
public String name
public Int ducklings
}
method Duck.say { return "quack!" }
method Duck.describe { return "{@name} the Duck; has {@ducklings} ducklings" }
Verbose equivalent:
method say {
in Duck self
out String message = "quack!"
}
method describe {
in Duck self
out String message = "{self.name} the Duck; has {self.ducklings} ducklings"
}
4. Static type system, with sum types AND first class types. Ie. types are treated as values (albeit mostly at compile-time), and can be declared using let, etc.
Type unions are treated as sets of other types:
let Number = Int | Float
assert(Number & Float == Float)
assert(Number ^ Float == Int)
The empty type union is None:
assert(Number & Float != None)
assert(Number & String == None)
assert(Number | None == Number)
Null values are accomplished via the special Null type, with one value / instance null:
let foo : String | Null = "fubar"
assert(foo != null)
assert(foo is String)
foo = null
assert(foo == null)
assert(foo is Null)
The dynamic type checking above applies to all other types:
let foo : String | Int = 10
assert(foo is Int)
assert(foo !is String)
foo = "fubar"
assert(foo is String)
assert(foo !is Int)
5. Parameterized types:
type Tree (T) {
public T | Null left = null
public T | Null right = null
}
let DuckTree = Tree(Duck)
6. Implicit pattern matching (this may be expensive):
type Duck {}
type Dog {}
method Duck.say { return "quack!" }
method Dog.say { return "woof!" }
let Animal = Duck | Dog
let animals = [
Duck(...),
Dog(...),
Duck(...),
Dog(...)
]
assert(animals is Animal[])
assert(animals is List(Animal))
assert(animals[0] is Duck && animals[1] is Dog)
assert(join(sep=", ", map(say, animals)) == "quack!, woof!, quack!, woof!")
7. Bind keyword / builtin function (partially applies arguments):
let add = &+
let add1 = bind(add, 1)
assert(add1(1) == 2)
method greet {
in String greeting
in String object
out String response = "${greeting}, ${object}!"
}
sayHello = bind(greet, greeting="Hello")
assert(sayHello("cat") == "Hello, cat!")
greetDog = bind(greet, object="dog")
assert(greetDog("Hello") == "Hello, dog!")
interfaces.gen.l2i
method foo {
in Arg1 arg1
in Arg2 arg2
in Arg3 arg3
out Result result
}
type Tree (T) {
public T | Null left = null
public T | Null right = null
}
method apply (T) {
in Tree(T) node
in Callable(T) fcn
}
method left (T) {
in Tree(T) node
out Tree(T) | Null result
}
method right (T) {
in Tree(T) node
out Tree(T) | Null result
}
method toList (T) {
in Tree(T) tree
out List(T) list
}
method toTree (T) {
in List(T) list
out Tree(T) tree
}
method cast (T) {
in Tree(T) tree
out String result
}
method cast (T) {
in Tree (T) tree
out List (T) result
}
method cast (T) {
in Tree (T) tree
out List (T) result
}
let Container (T) = Tree (T) | List (T) | Array (T)
--
type GLContext {}
type GLShader {}
type GLVertexBuffer {}
type GLTexture {}
type GLResource = GLShader | GLVertexBuffer | GLTexture
enum GLShaderType = GLFragmentShader | GLVertexShader | GLGeometryShader
method create (T) if (T & GLResource != None) {
mut GLContext context
out T | Null result
}
method setSource {
mut GLContext context
mut GLShader shader
in GLShaderType type
in String src
}
method bindResource (T) if (T & GLResource != None) {
mut GLContext context
in T | Null resource
out Bool result
}
Note that the above have a `mut GLContext` embedded? For GL code we could do the following:
package GL41 {
import GL as GL
let mut context = GL.createGLContext(GLVersion.GL41)
let create (T) = bind(GL.create(T), context = context)
let setSource = bind(GL.setSource, context = context)
let bindResource (T) = bind(GL.bindResource(T), context = context)
Note: no conflict of let x (T) = ... with function declaration, since we have a different syntax!
As such, let x (T) = ... always refers to template arguments... (type parameters)
Note 2: need to defer context.init to app startup somehow...
Or we encapsuate this entire thing as an object...?
}
Usage:
import GL41
local GLShader shader; create(result = shader) // this is really messy...
let GLShader shader = create() // this would work (neat solution; we can let type inference work from out parameters)
let shader = create() // error: does not specify a type
let shader = create!(Shader) // ok (using D's template syntax...)
Addendum: Having some kind of 'event' type might be useful. Or we reuse methods?
event AppInit { mut App application; in String[] args }
when(AppInit, defer(context.init))
Would that work? Events would be super-useful for other purposes ofc, and could store arguments...
Think of them as methods with no body, but which can be bound to by multiple methods?
Then we have problems with detachment, lifetime, etc., but could maybe be managed...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment