Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Implementation roadmap for a Rust-hosted Clojure dialect. Native file extension
- [ ] `derive` / full `isa?` hierarchy
- [x] `defrecord` — backed by `TypeInstance` (type_tag + MapValue fields); generates `->Name` and `map->Name` constructors; supports inline protocol impls
- [x] `reify` — creates a unique-tagged `TypeInstance`; supports inline protocol impls
- [ ] `deftype` — blocked by `.` interop (field access via `(.field obj)` not yet implemented; Phase 9); mutable fields require `set!`-on-field semantic that needs interop dot special form; low priority until Phase 9
- [x] `deftype` — positional `->Name` constructor, inline protocol impls with fields in scope, `.-field` access, and mutable fields (`^:unsynchronized-mutable` / `^:volatile-mutable`) written with `set!` (bare-symbol form inside a method and `(set! (.-field inst) v)` outside); no `map->Name` constructor, matching Clojure. Host-class method calls on an instance still await `.` interop (Phase 9)

---

Expand Down
1 change: 1 addition & 0 deletions crates/cljrs-compiler/src/rt_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,7 @@ pub unsafe extern "C" fn rt_assoc(
Value::TypeInstance(alloc_inner_coll(TypeInstance {
type_tag: ti.get().type_tag.clone(),
fields,
mutable: ti.get().mutable.clone(),
})),
meta,
))
Expand Down
8 changes: 8 additions & 0 deletions crates/cljrs-ir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ form. Interpreter-only special forms with no IR equivalent (`defprotocol`,
them as generic calls would resolve their clojure.core stub vars, which
return nil and silently corrupt the promoted function.

`set!` lowers to `SetBang` only for a global var target. A `deftype` mutable
field write — `(set! (.-field inst) v)`, or the bare `(set! field v)` inside a
method body, where the field is bound as a `let*` local — is likewise
**rejected**, because the `LoadVar`/`SetBang` pair would target a global var
of that name rather than the instance's interior-mutable cell: the write would
be lost and a stray var defined. The method tree-walks instead, where
`eval_set_bang` updates the cell.

`Const`, `LoadLocal`, `LoadGlobal`, `LoadVar`, `AllocVector`, `AllocMap`,
`AllocSet`, `AllocList`, `AllocCons`, `AllocClosure`, `CallKnown`, `Call`,
`CallDirect`, `Deref`, `DefVar`, `SetBang`, `Throw`, `Phi`, `Recur`,
Expand Down
16 changes: 14 additions & 2 deletions crates/cljrs-ir/src/lower/anf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2176,10 +2176,22 @@ fn lower_set_bang(ctx: &mut LowerCtx, args: &[Form]) -> R {
));
}
let FormKind::Symbol(sym_str) = &args[0].kind else {
return Err(LowerError::MalformedSpecialForm(
"set! target must be a symbol".into(),
// A non-symbol target, e.g. `(set! (.-field inst) v)` — a deftype
// mutable-field write. The IR var-store path cannot express it; decline
// to lower so the method tree-walks (eval_set_bang handles it).
return Err(LowerError::UnsupportedForm(
"set! on a non-symbol target (deftype mutable field)".into(),
));
};
// A set! whose target is a LOCAL binding is a deftype mutable-field write
// (the field is bound as a let* local in the synthesized method body). The
// var-store path would silently target a global var; decline to lower so
// the method tree-walks, where eval_set_bang updates the interior cell.
if ctx.lookup_local(sym_str).is_some() {
return Err(LowerError::UnsupportedForm(
"set! on a local binding (deftype mutable field)".into(),
));
}
let (var_ns, var_name) = split_sym(sym_str, ctx.ns());
let var_dst = ctx.fresh_var();
ctx.emit(Inst::LoadVar(var_dst, var_ns, var_name));
Expand Down
25 changes: 24 additions & 1 deletion crates/cljrs-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,22 @@ tests/
declare_macro.rs, doc.rs, gas_meter.rs, into_seq_target.rs, map_entry.rs,
defrecord_method_fields.rs — defrecord fields in scope in an inline
protocol method body; params shadow them
deftype_types.rs — deftype: positional ctor, `.-field`, protocol
impls, and mutable fields written with `set!`
qualified_protocol_impl.rs — a qualified protocol name in an impl position
(defrecord/deftype/reify/extend-*) resolves
through its own namespace
named_fn_identity.rs, ns_metadata.rs, partition_arities.rs, shared_atom.rs,
symbolic_nan.rs, threading_macros.rs, auto_gensym.rs, auto_keyword_macro.rs,
assoc_in_metadata.rs, empty_metadata.rs, into_metadata.rs, vec_metadata.rs,
defonce_metadata.rs, defonce_metadata_properties.rs,
auto_resolution_properties.rs — tree-walker behavior
gas_meter_ir.rs, versioned_ir.rs, partition_ir.rs, destructure_lowering.rs,
osr_transfer.rs, region_phi_uaf.rs — tiered behavior
deftype_mutable_tiered.rs — deftype mutable-field writes under forced
eager IR lowering: the lowerer must decline
a `set!` on a local (own binary — it flips
the process-wide eager-lowering switch)
```

---
Expand Down Expand Up @@ -964,7 +973,7 @@ implement sentinel operations without hitting the stub errors registered in
| `eval_eval(args, env)` | `eval` — convert a form value back to a `Form` and evaluate it at top level of the current namespace (vars visible, caller's locals not) |
| `eval_with_bindings_star(args, env)` | `with-bindings*` — push binding frame, call f |
| `eval_send_to_agent(args, env)` | `send` / `send-off` — dispatch action to agent |
| `dispatch_method(method, target, args)` | `(.method target args…)` — interop method dispatch on an evaluated target (strings, vectors, seqs) |
| `dispatch_method(method, target, args)` | `(.method target args…)` — interop method dispatch on an evaluated target (strings, vectors, seqs); on a `TypeInstance` only `.-field` reads are supported (mutable cell first, then the field map) |

`make_lazy_seq_from_fn(f, globals, ns)` (already public) creates a `LazySeq`
from a zero-arg callable; the above `make_delay_from_fn` is the analogous
Expand All @@ -981,6 +990,20 @@ conditional in ANY slot of an `ns` require spec, namespace included, so
`[#?(:clj clojure.core :cljs cljs.core) :as core]` reads — an option selecting
no branch is dropped, a namespace selecting none is an error.

`deftype` and `defrecord` share `parse_field_specs` (field name + mutability,
metadata-transparent), `build_positional_ctor` (`->Name`, routed to the
`make-type-instance-mut` builtin when the type declares mutable fields) and
`intern_type_symbol` (so `(instance? Name x)` resolves); only `defrecord` also
gets `build_map_ctor`. `synth_field_scope` wraps a method body in a `let*`
binding each field a param does not shadow — a mutable field through
`(.-field this)` (the live cell) and an immutable one through `(:field this)` —
plus a hidden `__deftype_self__` handle when any field is mutable, which is how
`eval_set_bang` finds the instance whose cell a bare `(set! field v)` updates.
`resolve_protocol_sym` resolves a protocol named in an impl position
(`extend-type`, `extend-protocol`, `reify`/`defrecord`/`deftype`) through the
current ns's `:require :as` aliases and through its own namespace when
qualified — not as a literal intern of the current ns.

---

## Module `tiered`
Expand Down
52 changes: 52 additions & 0 deletions crates/cljrs-runtime/src/builtins/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,11 @@ pub fn register_all(globals: &Arc<GlobalEnv>, ns: &str) {
Arity::Fixed(2),
builtin_make_type_instance,
),
(
"make-type-instance-mut",
Arity::Fixed(3),
builtin_make_type_instance_mut,
),
("record?", Arity::Fixed(1), builtin_record_q),
("instance?", Arity::Fixed(2), builtin_instance_q),
// Native objects (Phase 9 interop)
Expand Down Expand Up @@ -3642,6 +3647,7 @@ fn builtin_assoc(args: &[Value]) -> ValueResult<Value> {
return Ok(apply_meta(Value::TypeInstance(GcPtr::new(TypeInstance {
type_tag: ti.get().type_tag.clone(),
fields,
mutable: ti.get().mutable.clone(),
}))));
}
let mut result = match coll {
Expand Down Expand Up @@ -5610,6 +5616,7 @@ fn assoc_in_impl(m: Value, keys: &[Value], val: Value) -> ValueResult<Value> {
Value::TypeInstance(ti) => Value::TypeInstance(GcPtr::new(TypeInstance {
type_tag: ti.get().type_tag.clone(),
fields: ti.get().fields.assoc(k.clone(), updated),
mutable: ti.get().mutable.clone(),
})),
_ => Value::Map(MapValue::empty().assoc(k.clone(), updated)),
};
Expand Down Expand Up @@ -8229,6 +8236,51 @@ fn builtin_make_type_instance(args: &[Value]) -> ValueResult<Value> {
Ok(Value::TypeInstance(GcPtr::new(TypeInstance {
type_tag,
fields,
mutable: None,
})))
}

/// `(make-type-instance-mut type-tag immutable-map mutable-map)` — like
/// `make-type-instance`, but the keys in `mutable-map` become interior-mutable
/// slots (an `Atom` cell) that `set!` can update in place. Used by the `deftype`
/// positional constructor when the type declares `^:unsynchronized-mutable` or
/// `^:volatile-mutable` fields.
fn builtin_make_type_instance_mut(args: &[Value]) -> ValueResult<Value> {
let type_tag = match &args[0] {
Value::Str(s) => Arc::from(s.get().as_str()),
Value::Symbol(s) => Arc::from(s.get().name.as_ref()),
v => {
return Err(ValueError::WrongType {
expected: "string or symbol",
got: v.type_name().to_string(),
});
}
};
let fields = match &args[1] {
Value::Map(m) => m.clone(),
Value::Nil => MapValue::empty(),
v => {
return Err(ValueError::WrongType {
expected: "map",
got: v.type_name().to_string(),
});
}
};
let mut_map = match &args[2] {
Value::Map(m) => m.clone(),
Value::Nil => MapValue::empty(),
v => {
return Err(ValueError::WrongType {
expected: "map",
got: v.type_name().to_string(),
});
}
};
let cell = GcPtr::new(Atom::new(Value::Map(mut_map)));
Ok(Value::TypeInstance(GcPtr::new(TypeInstance {
type_tag,
fields,
mutable: Some(cell),
})))
}

Expand Down
23 changes: 23 additions & 0 deletions crates/cljrs-runtime/src/interp/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,29 @@ pub fn dispatch_method(method: &str, target: &Value, args: &[Value]) -> EvalResu
Value::List(_) | Value::Cons(_) | Value::LazySeq(_) => {
dispatch_seq_method(method, target, args)
}
Value::TypeInstance(ti) => {
// `.-field` reads a deftype/defrecord field. There are no host
// methods to call on an interpreter instance, so a plain `.method`
// is unsupported (protocol methods are called as `(proto-fn inst)`).
if let Some(field) = method.strip_prefix('-') {
let key = Value::keyword(cljrs_value::Keyword::simple(field));
let inst = ti.get();
// A mutable field lives in the interior-mutable cell; an
// immutable one in the field map.
if let Some(atom) = &inst.mutable
&& let Value::Map(m) = atom.get().deref()
&& let Some(v) = m.get(&key)
{
return Ok(v);
}
Ok(inst.fields.get(&key).unwrap_or(Value::Nil))
} else {
Err(EvalError::Runtime(format!(
".{method} not supported on {} (only .-field access is)",
target.type_name()
)))
}
}
_ => Err(EvalError::Runtime(format!(
".{method} not supported on type {}",
target.type_name()
Expand Down
Loading
Loading