Skip to content

Jock beta parser changes#62

Draft
sigilante wants to merge 8 commits into
masterfrom
sigilante/freshwater-ray-finned-fish
Draft

Jock beta parser changes#62
sigilante wants to merge 8 commits into
masterfrom
sigilante/freshwater-ray-finned-fish

Conversation

@sigilante
Copy link
Copy Markdown
Collaborator

This adds support for several new types and type system components. It breaks compatibility with Jock Alpha from June 2025.

@sigilante
Copy link
Copy Markdown
Collaborator Author

Posting a note here about where I was headed with the beta branch. The state is that I had just implemented the parser for native Path syntax (which NockApp needs) and I was working on alias next so we can use aliases for Wire.

The fundamental problem with the Jock alpha release was that the type system was ad hoc. That was fine for the time being, but obviously needed to be corrected. This showed up in two places:

  1. Syntax support (handling Set and List)
  2. class state and method definition

I opted to back out to a Rust-style impl operator. I had originally intended to remove the class keyword and do something like this. (I am eliding compose for a reason I'll explain later, and I'm sloppy with ; for the same reason. Doesn't matter for the exposition.)

struct Path(List<Chars>);  // corresponds to door sample
impl Path {
    head(self) -> ?Chars {
      // first segment
    }
    tail(self) -> Path {
      // rest of path
    }
    parent(self) -> Path {
      // all but last
    }
    leaf(self) -> ?Chars {
      // last segment
    }
};

I've recently decided that this introduces subject scope issues and it's better to retain class and explicitly corral the implementation, something like this.

class Path(List<Chars>) {
    head(self) -> ?Chars {
      // first segment
    }
    tail(self) -> Path {
      // rest of path
    }
    parent(self) -> Path {
      // all but last
    }
    leaf(self) -> ?Chars {
      // last segment
    }
    concat impl List {
      ...
    }
};

(You do want struct or type still for product types.)

Anyway, here's a summary of where I was going in this branch, modulo the class caveat above:

Struct

(Note: see class discussion above.)

struct Point(x: @ y: @);

Single-element structs:

alias Path List<Chars>; // transparent
struct UserId(Atom); // nominal wrapper

// Alias: no cast needed (see next section)
let p: Path = /foo/bar;
let q: List<Chars> = p; // OK

// Newtype: cast to unwrap
let id = UserId(42);
let raw = id as Atom; // OK, same shape
let bad: Atom = id; // Error: type mismatch

Aliases

alias Path List<Chars> // transparent alias
struct UserId(Atom) // opaque single-field wrapper
struct Point(x: Atom y: Atom) // named fields

These need to be resolved at compile time because they have to be set within the current subject scope, i.e. not completely lazily.

Example:

alias Path = List<Chars>;
let p = /foo/bar/(segment)/baz;
let q = /;
let r = /users/(user-id as Chars)/profile;
(p q r)

Traits

trait Arithmetic {
    add(self other:Self) -> Self
    neg(self) -> Self
}
  • Self is only type parameter (could be This or this; we have the latter in some form already but it's a little different still)
  • No associated types
  • No multi-param traits

I played a lot with terminology here. Also considered #1 and #, ## as options.

Trait Loss on Cast

impl Arithmetic for Vec2 { ... };

let v = Vec2(1 2);
v.add(Vec2(3 4))  // OK

let p:Point = v as Point;
p.add(Point(3 4))  // Error: no impl for Point

Cast changes type label. Impl lookup uses new type. Traits don't follow the data.

Union

union Result {
    case ok(value:Atom)
    case err(code:Atom msg:Chars)
}

Head-tagged cells: [%ok value], [%err code msg]

No generics on unions for v1.

Pattern matching via switch or match later, following ?+ pattern.

Higher-Order Functions

func map<T U>(f: T -> U  xs: List<T>) -> List<U> {
    ...
}
  • Explicit parameter types required
  • Explicit return type required
  • Type parameters: single uppercase letters
  • Forward inference in body
  • Monomorphized at call site

This feels more like a protocol/trait definition still and I want to play with alternatives. See Generics below.

Lambda

lambda (x: @ y: @) -> @ { x + y }

Explicit types required on parameters and return.

Object

object {
    func helper(n: @) -> @ { ... }
    constant = 42
}

Sample-less core, no $ arm. Arms accessible by name within scope.

Generics

func first<T>(xs: List<T>) -> ?T { ... }

let x = first([1 2 3])  // T = Atom, inferred from argument
let y = first([] as List<Atom>)  // T = Atom, explicit
  • Type parameters bound at call site from argument types
  • Empty collections require annotation
  • All generics monomorphized (no runtime polymorphism)

Option Chaining

e : T      e.f : U       →   e.f : U
e : ?T     e?.f : U      →   e?.f : ?U
e : ?T     e?.f : ?U     →   e?.f : ??U
e : ?T     d : T         →   e ?? d : T
e : ?T                   →   e! : T  (crash if none)

No flattening. ??T is valid and meaningful.

I actually really like this syntax for handling +units. We can even use it for empty tuples.

Casting

Syntax Compile-time Runtime Use
as structural check no same-shape nominal conversion
as? target known mold check returns ?T, safe narrowing
as! target known mold check returns T, crash on fail

Structural cast ignores field names:

struct Point(x:Real y:Real)
struct Coord(lat:Real lon:Real)

let c = Coord(1 2);
let p: Point = c as Point;

Subject-Oriented Method Resolution

The subject is a typed binary tree. As code composes:

compose
  with this; struct Point(x: @ y: @);

Subject type grows:

[[Arithmetic-for-Point arms] [Point-constructor prior-subject]]

Method resolution for x.method(args) where x: T:

  1. Walk subject tree
  2. Find arm named method where target type = T
  3. Emit Nock to grab arm at that axis and slam

Name collisions: disallowed in v1. Two traits with same method name = compile error. (Skip syntax ^ deferred.)

What I feel like would work better is a Dojo-style wrapping of everything in a =~, that way we don't have to explicitly compose everything.

Hoon Interop

let result: Noun = hoon.some-gate(arg)
let typed: MyStruct = result as! MyStruct
  • Hoon calls return Noun.
  • as! does runtime mold check (Hoon ;; semantics).
  • "Duck typing" at boundary: if noun shape matches, it works.

Jock always lets you lose type, sort of its version of unsafe. It just loses type when you use things like the Hoon FFI for most things that aren't simple. Then you use an as or ? option variant to reassert type natively.

Inference Rules Summary

Some initial thoughts, nothing binding here. It would be far more permissive than Hoon.

Context Inference
Function params Explicit required
Function return Explicit required
Lambda params Explicit required
Lambda return Explicit required
let binding Inferred from RHS, or explicit
Generic type params Inferred from arguments at call site
Empty collections Explicit annotation required
Mixed numeric literals Unify to supertype
Binary ops across types Determined by operator's trait impl

Naming

Convention Usage
kebab-case variables, functions, fields
PascalCase structs, traits, unions
T, U type parameters
  • x-1 = variable name
  • x - 1 = subtraction (mandatory spacing).
  • x -1 pair with unary negation on second one

This is because otherwise you will have head tags from Hoon and external nouns which are both different as raw atoms and unparseable as snake case values. BAD!

@sigilante
Copy link
Copy Markdown
Collaborator Author

Oh, and primitive types of course.

Type Notes
Atom / Uint unsigned integer, untyped atom
Int / Sint signed integer (ZigZag atom)
Hex @x not @ux (slightly different parsing)
String tape ((list @tD)) (note change here)
Chars cord (@t) (note change here)
Real (64-bit default) Real16, Real32, Real64, Real128
Logical true / false
Date @da
Span @dr
Noun noun (untyped escape hatch)

Compound Types

Type Syntax Hoon equivalent
Cell / Tuple (a b c) cell (but as +unit so can be empty)
List [a b c] +list (already can be empty, ~)
Set {a b c} +set (ditto)
Dict {k: v} (empty: {:}) +map (ditto)
Vector <a b c> (could change) Lagoon $ndray
Option ?a +unit

@sigilante
Copy link
Copy Markdown
Collaborator Author

Types necessary for NockApp kernel:

+$  goof    [mote=term =tang]
+$  wire    path
+$  ovum    [=wire =input]
+$  crud    [=goof =input]
+$  input   [eny=@ our=@ux now=@da cause=*]
alias Tang List<Chars>;
alias Term Chars;  // for now

struct Goof {
  mote: Term
  tang: Tang
}

alias Wire Path;

struct Input {
  eny: Uint
  our: Hex
  now: Date
  cause: Noun
}

struct Ovum {
  wire: Wire
  input: Input
}

struct Crud {
  goof: Goof
  input: Input
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant