prepare to add project_attributes/2 and attribute_goals/2
This commit is contained in:
@@ -146,6 +146,7 @@ The following predicates are built-in to rusty-wam.
|
|||||||
* `compound/1`
|
* `compound/1`
|
||||||
* `copy_term/2`
|
* `copy_term/2`
|
||||||
* `cyclic_term/1`
|
* `cyclic_term/1`
|
||||||
|
* `dif/2`
|
||||||
* `expand_goal/2`
|
* `expand_goal/2`
|
||||||
* `expand_term/2`
|
* `expand_term/2`
|
||||||
* `false/0`
|
* `false/0`
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ impl<'a, TermMarker: Allocator<'a>> CodeGenerator<TermMarker>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_target<Target, Iter>(&mut self, iter: Iter, term_loc: GenContext, is_exposed: bool) -> Vec<Target>
|
fn compile_target<Target, Iter>(&mut self, iter: Iter, term_loc: GenContext, is_exposed: bool)
|
||||||
|
-> Vec<Target>
|
||||||
where Target: CompilationTarget<'a>, Iter: Iterator<Item=TermRef<'a>>
|
where Target: CompilationTarget<'a>, Iter: Iterator<Item=TermRef<'a>>
|
||||||
{
|
{
|
||||||
let mut target = Vec::new();
|
let mut target = Vec::new();
|
||||||
@@ -489,6 +490,14 @@ impl<'a, TermMarker: Allocator<'a>> CodeGenerator<TermMarker>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn compile_unblocked_cut(&mut self, code: &mut Code, cell: &'a Cell<VarReg>)
|
||||||
|
{
|
||||||
|
let r = self.marker.get(Rc::new(String::from("!")));
|
||||||
|
cell.set(VarReg::Norm(r));
|
||||||
|
code.push(set_cp!(cell.get().norm()));
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_seq(&mut self, iter: ChunkedIterator<'a>, conjunct_info: &ConjunctInfo<'a>,
|
fn compile_seq(&mut self, iter: ChunkedIterator<'a>, conjunct_info: &ConjunctInfo<'a>,
|
||||||
code: &mut Code, is_exposed: bool)
|
code: &mut Code, is_exposed: bool)
|
||||||
-> Result<(), ParserError>
|
-> Result<(), ParserError>
|
||||||
@@ -518,7 +527,7 @@ impl<'a, TermMarker: Allocator<'a>> CodeGenerator<TermMarker>
|
|||||||
code.push(get_level_and_unify!(cell.get().norm()));
|
code.push(get_level_and_unify!(cell.get().norm()));
|
||||||
},
|
},
|
||||||
&QueryTerm::UnblockedCut(ref cell) =>
|
&QueryTerm::UnblockedCut(ref cell) =>
|
||||||
code.push(set_cp!(cell.get().norm())),
|
self.compile_unblocked_cut(code, cell),
|
||||||
&QueryTerm::BlockedCut =>
|
&QueryTerm::BlockedCut =>
|
||||||
code.push(if chunk_num == 0 {
|
code.push(if chunk_num == 0 {
|
||||||
Line::Cut(CutInstruction::NeckCut)
|
Line::Cut(CutInstruction::NeckCut)
|
||||||
|
|||||||
@@ -175,14 +175,14 @@ impl DebrayAllocator {
|
|||||||
let mut final_index = 0;
|
let mut final_index = 0;
|
||||||
|
|
||||||
for index in self.temp_lb .. {
|
for index in self.temp_lb .. {
|
||||||
if !self.in_use.contains(&index) {
|
if !self.in_use.contains(&index) {
|
||||||
final_index = index;
|
final_index = index;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.in_use.insert(final_index);
|
||||||
self.temp_lb = final_index + 1;
|
self.temp_lb = final_index + 1;
|
||||||
|
|
||||||
final_index
|
final_index
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,10 @@ impl<'a> Allocator<'a> for DebrayAllocator
|
|||||||
RegType::Temp(k)
|
RegType::Temp(k)
|
||||||
},
|
},
|
||||||
_ if r.reg_num() == 0 => RegType::Temp(self.alloc_reg_to_non_var()),
|
_ if r.reg_num() == 0 => RegType::Temp(self.alloc_reg_to_non_var()),
|
||||||
_ => r
|
_ => {
|
||||||
|
self.in_use.insert(r.reg_num());
|
||||||
|
r
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cell.set(r);
|
cell.set(r);
|
||||||
|
|||||||
@@ -124,11 +124,10 @@ impl<'a> VariableFixtures<'a>
|
|||||||
status.1.push(cell);
|
status.1.push(cell);
|
||||||
|
|
||||||
match status.0 {
|
match status.0 {
|
||||||
VarStatus::Temp(cn, ref mut tvd) if cn == chunk_num => {
|
VarStatus::Temp(cn, ref mut tvd) if cn == chunk_num =>
|
||||||
if let Level::Shallow = lvl {
|
if let Level::Shallow = lvl {
|
||||||
self.record_temp_info(tvd, arg_c, term_loc);
|
self.record_temp_info(tvd, arg_c, term_loc);
|
||||||
}
|
},
|
||||||
},
|
|
||||||
_ => status.0 = VarStatus::Perm(chunk_num)
|
_ => status.0 = VarStatus::Perm(chunk_num)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -950,7 +950,7 @@ pub enum LocalCodePtr {
|
|||||||
UserTermExpansion(usize)
|
UserTermExpansion(usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalCodePtr {
|
impl LocalCodePtr {
|
||||||
pub fn assign_if_local(&mut self, cp: CodePtr) {
|
pub fn assign_if_local(&mut self, cp: CodePtr) {
|
||||||
match cp {
|
match cp {
|
||||||
CodePtr::Local(local) => *self = local,
|
CodePtr::Local(local) => *self = local,
|
||||||
|
|||||||
57
src/prolog/lib/dif.pl
Normal file
57
src/prolog/lib/dif.pl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
[:- module(dif, [dif/2]).
|
||||||
|
|
||||||
|
:- use_module(library(atts)).
|
||||||
|
:- use_module(library(control), [(\=)/2]).
|
||||||
|
|
||||||
|
:- attribute dif/2.
|
||||||
|
|
||||||
|
dif_set_variables([], _, _).
|
||||||
|
dif_set_variables([Var|Vars], X, Y) :-
|
||||||
|
put_atts(Var, dif(X, Y)),
|
||||||
|
dif_set_variables(Vars, X, Y).
|
||||||
|
|
||||||
|
verify_dif_attrs([dif(X, Y) | Attrs], Var, [X \== Y | Goals]) :-
|
||||||
|
( get_atts(Var, +dif(X, Y)) -> true
|
||||||
|
; put_atts(Var, +dif(X, Y))
|
||||||
|
),
|
||||||
|
verify_dif_attrs(Attrs, Var, Goals).
|
||||||
|
verify_dif_attrs([_ | Attrs], Var, Goals) :-
|
||||||
|
verify_dif_attrs(Attrs, Var, Goals).
|
||||||
|
verify_dif_attrs([], _, []).
|
||||||
|
|
||||||
|
verify_dif_attrs_no_var([dif(X, Y) | Attrs], [X \== Y | Goals]) :-
|
||||||
|
verify_dif_attrs_no_var(Attrs, Goals).
|
||||||
|
verify_dif_attrs_no_var([_ | Attrs], Goals) :-
|
||||||
|
verify_dif_attrs_no_var(Attrs, Goals).
|
||||||
|
verify_dif_attrs_no_var([], []).
|
||||||
|
|
||||||
|
verify_attributes(Var, Value, Goals) :-
|
||||||
|
( get_atts(Var, Attrs) ->
|
||||||
|
( var(Value) -> verify_dif_attrs(Attrs, Value, Goals)
|
||||||
|
; verify_dif_attrs_no_var(Attrs, Goals)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
% Probably the world's worst dif/2 implementation. I'm open to
|
||||||
|
% suggestions for improvement.
|
||||||
|
|
||||||
|
dif(X, Y) :- ( X \= Y -> true
|
||||||
|
; term_variables(X, XVars), term_variables(Y, YVars),
|
||||||
|
( XVars == [], YVars == [] -> false
|
||||||
|
; dif_set_variables(XVars, X, Y),
|
||||||
|
dif_set_variables(YVars, X, Y)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
|
gather_dif_goals(Attrs, Goal, Goal) :-
|
||||||
|
var(Attrs), !.
|
||||||
|
gather_dif_goals([dif(X, Y)|Attrs], OldGoal, Goal) :-
|
||||||
|
( var(OldGoal), !, gather_dif_goals(Attrs, dif(X, Y), Goal)
|
||||||
|
; !, gather_dif_goals(Attrs, (dif(X, Y), OldGoal), Goal)
|
||||||
|
).
|
||||||
|
gather_dif_goals([_|Attrs], OldGoal, Goal) :-
|
||||||
|
gather_dif_goals(Attrs, OldGoal, Goal).
|
||||||
|
|
||||||
|
attribute_goals(X, Goal) :-
|
||||||
|
'$get_attr_list'(X, Attrs),
|
||||||
|
gather_dif_goals(Attrs, _, Goal).
|
||||||
@@ -18,7 +18,7 @@ gather_modules([Attr|Attrs], [Module|Modules]) :-
|
|||||||
|
|
||||||
verify_attrs([Module|Modules], Var, Value, [Goals|ListOfGoalLists]) :-
|
verify_attrs([Module|Modules], Var, Value, [Goals|ListOfGoalLists]) :-
|
||||||
catch(Module:verify_attributes(Var, Value, Goals),
|
catch(Module:verify_attributes(Var, Value, Goals),
|
||||||
error(evaluation_error((M:verify_attributes)/3), verify_attrs/3),
|
error(evaluation_error((Module:verify_attributes)/3), verify_attributes/3),
|
||||||
Goals = []),
|
Goals = []),
|
||||||
verify_attrs(Modules, Var, Value, ListOfGoalLists).
|
verify_attrs(Modules, Var, Value, ListOfGoalLists).
|
||||||
verify_attrs([], _, _, []).
|
verify_attrs([], _, _, []).
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
use prolog::machine::*;
|
use prolog::machine::*;
|
||||||
|
|
||||||
pub static VERIFY_ATTRS: &str = include_str!("attributed_variables.pl");
|
pub static VERIFY_ATTRS: &str = include_str!("attributed_variables.pl");
|
||||||
|
pub static PROJECT_ATTRS: &str = include_str!("project_attributes.pl");
|
||||||
|
|
||||||
pub(super) type Bindings = Vec<(usize, Addr)>;
|
pub(super) type Bindings = Vec<(usize, Addr)>;
|
||||||
|
|
||||||
pub(super) struct AttrVarInitializer {
|
pub(super) struct AttrVarInitializer {
|
||||||
pub(super) bindings: Bindings,
|
pub(super) bindings: Bindings,
|
||||||
pub(super) cp: LocalCodePtr,
|
pub(super) cp: LocalCodePtr,
|
||||||
pub(super) verify_attrs_loc: usize
|
pub(super) verify_attrs_loc: usize,
|
||||||
|
pub(super) project_attrs_loc: usize
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttrVarInitializer {
|
impl AttrVarInitializer {
|
||||||
pub(super) fn new(p: usize) -> Self {
|
pub(super) fn new(verify_attrs_loc: usize, project_attrs_loc: usize) -> Self {
|
||||||
AttrVarInitializer {
|
AttrVarInitializer {
|
||||||
bindings: vec![],
|
bindings: vec![],
|
||||||
cp: LocalCodePtr::default(),
|
cp: LocalCodePtr::default(),
|
||||||
verify_attrs_loc: p,
|
verify_attrs_loc,
|
||||||
|
project_attrs_loc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ impl MachineState {
|
|||||||
e: 0,
|
e: 0,
|
||||||
num_of_args: 0,
|
num_of_args: 0,
|
||||||
cp: LocalCodePtr::default(),
|
cp: LocalCodePtr::default(),
|
||||||
attr_var_init: AttrVarInitializer::new(0),
|
attr_var_init: AttrVarInitializer::new(0, 0),
|
||||||
fail: false,
|
fail: false,
|
||||||
heap: Heap::with_capacity(256),
|
heap: Heap::with_capacity(256),
|
||||||
mode: MachineMode::Write,
|
mode: MachineMode::Write,
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ static ERROR: &str = include_str!("../lib/error.pl");
|
|||||||
static TERMS: &str = include_str!("../lib/terms.pl");
|
static TERMS: &str = include_str!("../lib/terms.pl");
|
||||||
static DCGS: &str = include_str!("../lib/dcgs.pl");
|
static DCGS: &str = include_str!("../lib/dcgs.pl");
|
||||||
static ATTS: &str = include_str!("../lib/atts.pl");
|
static ATTS: &str = include_str!("../lib/atts.pl");
|
||||||
|
static DIF: &str = include_str!("../lib/dif.pl");
|
||||||
|
|
||||||
impl Machine {
|
impl Machine {
|
||||||
fn compile_special_forms(&mut self) {
|
fn compile_special_forms(&mut self) {
|
||||||
@@ -327,8 +328,27 @@ impl Machine {
|
|||||||
self.machine_st.attr_var_init.verify_attrs_loc = self.code_repo.code.len();
|
self.machine_st.attr_var_init.verify_attrs_loc = self.code_repo.code.len();
|
||||||
self.code_repo.code.extend(code.into_iter());
|
self.code_repo.code.extend(code.into_iter());
|
||||||
},
|
},
|
||||||
Err(_) => panic!("Machine::compile_special_forms() failed")
|
Err(_) => panic!("Machine::compile_special_forms() failed at VERIFY_ATTRS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match compile_special_form(self, PROJECT_ATTRS.as_bytes()) {
|
||||||
|
Ok(code) => {
|
||||||
|
self.machine_st.attr_var_init.project_attrs_loc = self.code_repo.code.len();
|
||||||
|
self.code_repo.code.extend(code.into_iter());
|
||||||
|
},
|
||||||
|
Err(_) => panic!("Machine::compile_special_forms() failed at PROJECT_ATTRS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_libraries(&mut self) {
|
||||||
|
compile_user_module(self, LISTS.as_bytes());
|
||||||
|
compile_user_module(self, CONTROL.as_bytes());
|
||||||
|
compile_user_module(self, QUEUES.as_bytes());
|
||||||
|
compile_user_module(self, ERROR.as_bytes());
|
||||||
|
compile_user_module(self, TERMS.as_bytes());
|
||||||
|
compile_user_module(self, DCGS.as_bytes());
|
||||||
|
compile_user_module(self, ATTS.as_bytes());
|
||||||
|
compile_user_module(self, DIF.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -344,15 +364,9 @@ impl Machine {
|
|||||||
compile_listing(&mut wam, BUILTINS.as_bytes(),
|
compile_listing(&mut wam, BUILTINS.as_bytes(),
|
||||||
default_index_store!(atom_tbl.clone()));
|
default_index_store!(atom_tbl.clone()));
|
||||||
|
|
||||||
compile_user_module(&mut wam, LISTS.as_bytes());
|
wam.compile_libraries();
|
||||||
compile_user_module(&mut wam, CONTROL.as_bytes());
|
|
||||||
compile_user_module(&mut wam, QUEUES.as_bytes());
|
|
||||||
compile_user_module(&mut wam, ERROR.as_bytes());
|
|
||||||
compile_user_module(&mut wam, TERMS.as_bytes());
|
|
||||||
compile_user_module(&mut wam, DCGS.as_bytes());
|
|
||||||
compile_user_module(&mut wam, ATTS.as_bytes());
|
|
||||||
|
|
||||||
wam.compile_special_forms();
|
wam.compile_special_forms();
|
||||||
|
|
||||||
wam
|
wam
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +622,7 @@ impl MachineState {
|
|||||||
for i in 1 .. rs + 1 {
|
for i in 1 .. rs + 1 {
|
||||||
self.and_stack[e][i] = self[RegType::Temp(i)].clone();
|
self.and_stack[e][i] = self[RegType::Temp(i)].clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.and_stack[e][rs + 1] = Addr::Con(Constant::Usize(self.b0));
|
self.and_stack[e][rs + 1] = Addr::Con(Constant::Usize(self.b0));
|
||||||
self.and_stack[e][rs + 2] = Addr::Con(Constant::Usize(self.num_of_args));
|
self.and_stack[e][rs + 2] = Addr::Con(Constant::Usize(self.num_of_args));
|
||||||
|
|
||||||
|
|||||||
46
src/prolog/machine/project_attributes.pl
Normal file
46
src/prolog/machine/project_attributes.pl
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
:- use_module(library(dcgs)).
|
||||||
|
|
||||||
|
driver(QueryVars, AttrVars) :-
|
||||||
|
phrase(gather_modules(AttrVars), Modules0),
|
||||||
|
sort(Modules0, Modules),
|
||||||
|
call_project_attributes(Modules, QueryVars, AttrVars),
|
||||||
|
call_attribute_goals(QueryVars),
|
||||||
|
call_attribute_goals(AttrVars).
|
||||||
|
|
||||||
|
call_project_attributes([], _, _).
|
||||||
|
call_project_attributes([Module|Modules], QueryVars, AttrVars) :-
|
||||||
|
( catch(Module:project_attributes(QueryVars, AttrVars),
|
||||||
|
error(evaluation_error((Module:project_attributes/2), project_attributes/2)),
|
||||||
|
true) -> true
|
||||||
|
; true
|
||||||
|
),
|
||||||
|
call_project_attributes(Modules, QueryVars, AttrVars).
|
||||||
|
|
||||||
|
call_attribute_goals([]).
|
||||||
|
call_attribute_goals([AttrVar | AttrVars]) :-
|
||||||
|
( '$get_attr_list'(AttrVar, Ls) -> call_goals(Ls, AttrVar)
|
||||||
|
; true
|
||||||
|
),
|
||||||
|
call_attribute_goals(AttrVars).
|
||||||
|
|
||||||
|
call_goals(Ls, _) :- var(Ls), !.
|
||||||
|
call_goals([Attr|Attrs], X) :-
|
||||||
|
'$module_of'(Module, Attr),
|
||||||
|
( catch(Module:attribute_goals(X, Goal),
|
||||||
|
error(evaluation_error((Module:attribute_goals/2), attribute_goals/2)),
|
||||||
|
writeq(Goal)) -> true
|
||||||
|
; true
|
||||||
|
),
|
||||||
|
call_goals(Attrs, X).
|
||||||
|
|
||||||
|
gather_modules([]) --> [].
|
||||||
|
gather_modules([AttrVar|AttrVars]) -->
|
||||||
|
{ '$get_attr_list'(AttrVar, Attrs) },
|
||||||
|
gather_modules_for_attrs(Attrs),
|
||||||
|
gather_modules(AttrVars).
|
||||||
|
|
||||||
|
gather_modules_for_attrs(Attrs) --> { var(Attrs), ! }.
|
||||||
|
gather_modules_for_attrs([Attr|Attrs]) -->
|
||||||
|
{ '$module_of'(Module, Attr) },
|
||||||
|
[Module],
|
||||||
|
gather_modules_for_attrs(Attrs).
|
||||||
@@ -104,8 +104,7 @@ impl<'a> CompilationTarget<'a> for QueryInstruction {
|
|||||||
post_order_iter(term)
|
post_order_iter(term)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_structure(ct: ClauseType, arity: usize, r: RegType) -> Self
|
fn to_structure(ct: ClauseType, arity: usize, r: RegType) -> Self {
|
||||||
{
|
|
||||||
QueryInstruction::PutStructure(ct, arity, r)
|
QueryInstruction::PutStructure(ct, arity, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ fn mark_cut_variables(terms: &mut Vec<Term>) -> bool {
|
|||||||
let mut found_cut_var = false;
|
let mut found_cut_var = false;
|
||||||
|
|
||||||
for item in terms.iter_mut() {
|
for item in terms.iter_mut() {
|
||||||
found_cut_var = mark_cut_variable(item);
|
found_cut_var = mark_cut_variable(item) || found_cut_var;
|
||||||
}
|
}
|
||||||
|
|
||||||
found_cut_var
|
found_cut_var
|
||||||
@@ -437,22 +437,16 @@ impl RelationWorker {
|
|||||||
|
|
||||||
fn fabricate_disjunct(&self, body_term: Term) -> (JumpStub, VecDeque<Term>)
|
fn fabricate_disjunct(&self, body_term: Term) -> (JumpStub, VecDeque<Term>)
|
||||||
{
|
{
|
||||||
let mut cut_var_found = false;
|
let vars = self.compute_head(&body_term);
|
||||||
|
|
||||||
let mut vars = self.compute_head(&body_term);
|
|
||||||
let clauses: Vec<_> = unfold_by_str(body_term, ";").into_iter()
|
let clauses: Vec<_> = unfold_by_str(body_term, ";").into_iter()
|
||||||
.map(|term| {
|
.map(|term| {
|
||||||
let mut subterms = unfold_by_str(term, ",");
|
let mut subterms = unfold_by_str(term, ",");
|
||||||
cut_var_found = mark_cut_variables(&mut subterms);
|
mark_cut_variables(&mut subterms);
|
||||||
|
|
||||||
let term = subterms.pop().unwrap();
|
let term = subterms.pop().unwrap();
|
||||||
fold_by_str(subterms.into_iter(), term, clause_name!(","))
|
fold_by_str(subterms.into_iter(), term, clause_name!(","))
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
if cut_var_found {
|
|
||||||
vars.push(Term::Var(Cell::default(), rc_atom!("!")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = clauses.into_iter()
|
let results = clauses.into_iter()
|
||||||
.map(|clause| self.fabricate_rule_body(&vars, clause))
|
.map(|clause| self.fabricate_rule_body(&vars, clause))
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ impl fmt::Display for CompareTermQT {
|
|||||||
impl fmt::Display for ClauseType {
|
impl fmt::Display for ClauseType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
&ClauseType::System(SystemClauseType::SetCutPoint(r)) =>
|
||||||
|
write!(f, "$set_cp({})", r),
|
||||||
&ClauseType::Named(ref name, ref idx) | &ClauseType::Op(OpDecl(.., ref name), ref idx) =>
|
&ClauseType::Named(ref name, ref idx) | &ClauseType::Op(OpDecl(.., ref name), ref idx) =>
|
||||||
{
|
{
|
||||||
let idx = idx.0.borrow();
|
let idx = idx.0.borrow();
|
||||||
|
|||||||
Reference in New Issue
Block a user