wip restructuring
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Aegora Base Contract
|
||||
*
|
||||
* Author: Craig Everett <ceverett@tsuriai.jp>
|
||||
* Copyright: Tsuriai Corporation (2022)
|
||||
* License: GPLv3
|
||||
* Version: 1
|
||||
*
|
||||
* This is the base contract for the market.
|
||||
* It is responsible for:
|
||||
* - A library of valid contracts by type (sales offers, auctions, etc.)
|
||||
* - Managing the authorized key list of market masters
|
||||
* - Providing a single known endpoint to discover deployed, active contracts
|
||||
* - Managing the lifecycle of a contract
|
||||
*
|
||||
* Lifecycle of a sales offer:
|
||||
* 1. The seller calls AegoraBase.post_sale() to create his SalesOffer
|
||||
* 2. Sale proceeds
|
||||
* IF it is succesful, the contract calls AegoraBase.close()
|
||||
* IF it is revoked by seller, the contract calls AegoraBase.close()
|
||||
* IF it times out or is killed by the maester, one calls AegoraBase.kill()
|
||||
*
|
||||
* Calls to this contract can come from one of five sources:
|
||||
* 1. The Maester (essentially the administrator)
|
||||
* - init(template : SalesOffer // Posts the initial contract
|
||||
* maester : address // with a reference to a template
|
||||
* tsuriai : address)
|
||||
* - update_template(contract : SalesOffer) // Update the template address
|
||||
* - epstein(id : bool) // Force a contract to close
|
||||
* 2. Sellers
|
||||
* - post_sale(id : int, price : int) // Create a new sale contract clone
|
||||
* 3. The Aegora site (or anyone really, but mainly useful for the site backend)
|
||||
* - lookup(id) // Looks up a sale contract's address
|
||||
* - template() // Looks up the current sales contract template
|
||||
* 4. A sales contract
|
||||
* - close(id : int) // A closing sale contract may call to remove itself from the active index
|
||||
* 5. The Tsuriai master Key
|
||||
* - update_maester(key : address) // Change the Maester key for operational reasons
|
||||
*/
|
||||
|
||||
@compiler == 7.0.1
|
||||
|
||||
payable contract interface SalesOffer =
|
||||
entrypoint init : (address, address, address, int, address, int, int) => void
|
||||
payable entrypoint do_a_backflip : () => bool
|
||||
|
||||
|
||||
contract AegoraBase =
|
||||
record state =
|
||||
{contracts : map(int, SalesOffer),
|
||||
template : SalesOffer,
|
||||
maester : address,
|
||||
tsuriai : address}
|
||||
|
||||
stateful entrypoint init(template : SalesOffer,
|
||||
maester : address,
|
||||
tsuriai : address) : state =
|
||||
{contracts = {},
|
||||
template = template,
|
||||
maester = maester,
|
||||
tsuriai = tsuriai}
|
||||
|
||||
public entrypoint template() : SalesOffer =
|
||||
state.template
|
||||
|
||||
public stateful entrypoint update_template(source : SalesOffer) : unit =
|
||||
require(Call.caller == state.maester, "Nuh, uh uh! You didn't say the magic word!")
|
||||
put(state{template = source})
|
||||
|
||||
public entrypoint lookup(id: int) : SalesOffer =
|
||||
switch(Map.lookup(id, state.contracts))
|
||||
Some(target) =>
|
||||
target
|
||||
None =>
|
||||
abort("Bad ID!")
|
||||
|
||||
public stateful entrypoint post_sale(agent : address, portion : int, seller : address, id: int, price: int) : SalesOffer =
|
||||
require(price > 0, "You cannot pay someone to buy things, Mr. Keynes.")
|
||||
require(!Map.member(id, state.contracts), "People don't think it be like it is, but it do.")
|
||||
switch(Chain.clone(ref = state.template,
|
||||
protected = true,
|
||||
state.tsuriai,
|
||||
Contract.address,
|
||||
agent,
|
||||
portion,
|
||||
seller,
|
||||
id,
|
||||
price))
|
||||
Some(posted) =>
|
||||
put(state{contracts = state.contracts{[id] = posted}})
|
||||
posted
|
||||
None =>
|
||||
abort("Bad sale!")
|
||||
|
||||
public stateful entrypoint close(id: int) : bool =
|
||||
switch(Map.lookup(id, state.contracts))
|
||||
Some(target) =>
|
||||
require(target.address == Call.caller, "Bad caller")
|
||||
put(state{contracts = Map.delete(id, state.contracts)})
|
||||
true
|
||||
None =>
|
||||
false
|
||||
|
||||
public stateful entrypoint update_maester(key: address) : unit =
|
||||
require(Call.caller == state.tsuriai, "Nuh, uh uh! You didn't say the magic word!")
|
||||
put(state{maester = key})
|
||||
|
||||
public stateful entrypoint epstein(id: int) : bool =
|
||||
require(Call.caller == state.maester, "C'mon, man! You only got the husband and son!")
|
||||
switch(Map.lookup(id, state.contracts))
|
||||
Some(target) =>
|
||||
put(state{contracts = Map.delete(id, state.contracts)})
|
||||
target.do_a_backflip()
|
||||
true
|
||||
None =>
|
||||
false
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Aegora Sales Offer
|
||||
*
|
||||
* Author: Craig Everett <ceverett@tsuriai.jp>
|
||||
* Copyright: Tsuriai Corporation (2022)
|
||||
* License: GPLv3
|
||||
* Version: 1
|
||||
*
|
||||
* Sale offers at Aegora (aegora.psychobitch.party) are clones of this contract.
|
||||
* (The name might be a little weird, but who looks at domain names anymore anyway?)
|
||||
* When a sale offer is created on the site, the seller signs a contract call to
|
||||
*
|
||||
*
|
||||
* Calls to clones of this contract can come from five sources:
|
||||
* 1. The base contract (that cloned it in the first place)
|
||||
* - init(tsuriai : address, // Service fee destination
|
||||
* aegora : AegoraBase, // Get born
|
||||
* agent : address, // Deployment agent address
|
||||
* portion : int, // Deploying agent's fee portion
|
||||
* seller : address, // Address of the seller
|
||||
* id : int,
|
||||
* price : int)
|
||||
* - do_a_backflip() // Get the opposite of born
|
||||
* 2. Sellers who posted clones of this contract
|
||||
* - adjust(price : int) // update price
|
||||
* - accept() // Sale is DONE
|
||||
* - refuse() // Cancel a negotation
|
||||
* - revoke() // Invalidate and disable this offer
|
||||
* 3. Buyers who are interested in buying through this contract
|
||||
* - bid(price : int) // Update the bid amout in negotiation
|
||||
* - hold() // Set HOLD for price negotiation
|
||||
* - cancel() // Back out of a purchase
|
||||
* 4. Aegora (the base contract)
|
||||
* - reassign(tsuriai : address) // Reset Tsuriai's payable address
|
||||
* 5. The market's network service (or the public)
|
||||
* - price() // Check current contract price
|
||||
* - status() // Check contract status
|
||||
* - agent() // Retrieve the agent's public key (usually the same as the seller)
|
||||
* - seller() // Retrieve the seller's public key
|
||||
* - buyer() // Retrieve the buyer's public key
|
||||
*/
|
||||
|
||||
@compiler == 7.0.1
|
||||
|
||||
include "Option.aes"
|
||||
|
||||
contract interface AegoraBase =
|
||||
stateful entrypoint close : (int) => bool
|
||||
|
||||
|
||||
payable contract SalesOffer =
|
||||
record state =
|
||||
{id : int,
|
||||
aegora : address,
|
||||
agent : address,
|
||||
portion : int,
|
||||
price : int,
|
||||
seller : address,
|
||||
buyer : option(address),
|
||||
status : status,
|
||||
tsuriai : address}
|
||||
|
||||
datatype status = OPEN | NEGO | HOLD | DONE
|
||||
|
||||
|
||||
// Seller Interface
|
||||
stateful entrypoint init(tsuriai : address,
|
||||
aegora : address,
|
||||
agent : address,
|
||||
portion : int,
|
||||
seller : address,
|
||||
id : int,
|
||||
price : int) : state =
|
||||
{id = id,
|
||||
aegora = aegora,
|
||||
agent = Call.origin,
|
||||
portion = portion,
|
||||
price = price,
|
||||
seller = seller,
|
||||
buyer = None,
|
||||
status = OPEN,
|
||||
tsuriai = tsuriai}
|
||||
|
||||
public stateful entrypoint adjust(price : int) : unit =
|
||||
require(state.status != DONE, "HiLlArY wUz HeRe.")
|
||||
require(Call.caller == state.seller, "Nacho shop!")
|
||||
require(price > 0, "You can't pay people to buy things, Mr. Keynes.")
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
if(Contract.balance > price)
|
||||
Chain.spend(buyer, Contract.balance - price)
|
||||
true
|
||||
else
|
||||
false
|
||||
None =>
|
||||
dead_claim()
|
||||
put(state{price = price})
|
||||
|
||||
public stateful entrypoint accept() : bool =
|
||||
require(state.status == NEGO, "Sale is not under negotiation.")
|
||||
require(Call.caller == state.seller, "Nacho shop!")
|
||||
require(Contract.balance >= state.price, "Insufficient funds!")
|
||||
let buyer = Option.force(state.buyer)
|
||||
if(Contract.balance > state.price)
|
||||
Chain.spend(buyer, Contract.balance - state.price)
|
||||
Chain.spend(state.tsuriai, calc_fee())
|
||||
if(state.portion > 0)
|
||||
Chain.spend(state.agent, (Contract.balance / state.portion))
|
||||
Chain.spend(state.seller, Contract.balance)
|
||||
put(state{status = DONE})
|
||||
Address.to_contract(state.aegora).close(state.id)
|
||||
|
||||
public stateful entrypoint refuse() : unit =
|
||||
require(state.status == NEGO || state.status == HOLD, "Sale is not under negotiation.")
|
||||
require(Call.caller == state.seller, "Nacho shop!")
|
||||
refund()
|
||||
put(state{buyer = None})
|
||||
put(state{status = OPEN})
|
||||
|
||||
public stateful entrypoint revoke() : bool =
|
||||
require(state.status != DONE, "HiLlArY wUz HeRe.")
|
||||
require(Call.caller == state.seller, "Nacho shop!")
|
||||
switch(state.status)
|
||||
OPEN => dead_claim()
|
||||
NEGO => refund()
|
||||
HOLD => refund()
|
||||
put(state{status = DONE})
|
||||
Address.to_contract(state.aegora).close(state.id)
|
||||
|
||||
|
||||
// Buyer Interface
|
||||
public stateful payable entrypoint bid(amount: int) : unit =
|
||||
require(state.status != DONE, "HiLlArY wUz HeRe.")
|
||||
require(amount >= state.price, "Stop being poor.")
|
||||
switch(state.status)
|
||||
OPEN =>
|
||||
require(Call.value >= state.price, "Stop being poor.")
|
||||
put(state{status = NEGO})
|
||||
put(state{buyer = Some(Call.caller)})
|
||||
NEGO =>
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
require(Call.caller == buyer, "Nacho bid.")
|
||||
require(Contract.balance >= state.price, "Stop being poor.")
|
||||
if(Contract.balance > amount)
|
||||
Chain.spend(buyer, Contract.balance - amount)
|
||||
HOLD =>
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
require(Call.caller == buyer, "Nacho bid.")
|
||||
require(Contract.balance >= state.price, "Stop being poor.")
|
||||
if(Contract.balance > amount)
|
||||
Chain.spend(buyer, Contract.balance - amount)
|
||||
put(state{status = NEGO})
|
||||
|
||||
public stateful entrypoint hold() : unit =
|
||||
require(state.status == NEGO, "Sale is not in negotiation")
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
require(Call.caller == buyer, "Nacho bid!")
|
||||
put(state{status = HOLD})
|
||||
None =>
|
||||
abort("Nacho bid!")
|
||||
|
||||
public stateful entrypoint cancel() : unit =
|
||||
require(state.status == NEGO || state.status == HOLD, "Wrong status")
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
require(Call.caller == buyer, "Nacho bid!")
|
||||
Chain.spend(state.tsuriai, calc_fee())
|
||||
put(state{buyer = None})
|
||||
put(state{status = OPEN})
|
||||
Chain.spend(buyer, Contract.balance)
|
||||
None =>
|
||||
abort("Nacho bid!")
|
||||
|
||||
|
||||
// Aegora Interface
|
||||
public stateful entrypoint reassign(tsuriai : address) : unit =
|
||||
require(Call.caller == state.aegora || Call.origin == state.tsuriai, "Knock it off, stinky")
|
||||
put(state{tsuriai = tsuriai})
|
||||
|
||||
public stateful entrypoint sweep_the_table() : bool =
|
||||
require(state.status == DONE, "Hillary has not yet arrived.")
|
||||
require(Call.caller == state.tsuriai, "Nope.")
|
||||
dead_claim()
|
||||
|
||||
public stateful entrypoint do_a_backflip() : bool =
|
||||
require(Call.caller == state.aegora, "Too late, Nathan.")
|
||||
put(state{status = DONE})
|
||||
dead_claim()
|
||||
|
||||
|
||||
// Service/Public Network Interface
|
||||
public entrypoint price() : int =
|
||||
state.price
|
||||
|
||||
public entrypoint status() : string =
|
||||
switch(state.status)
|
||||
OPEN => "open"
|
||||
NEGO => "nego"
|
||||
HOLD => "hold"
|
||||
DONE => "done"
|
||||
|
||||
public entrypoint agent() : address =
|
||||
state.agent
|
||||
|
||||
public entrypoint seller() : address =
|
||||
state.seller
|
||||
|
||||
public entrypoint buyer() : address =
|
||||
require(state.status != OPEN, "No buyer!")
|
||||
Option.force(state.buyer)
|
||||
|
||||
// Utilities
|
||||
function calc_fee() : int =
|
||||
Contract.balance / 50
|
||||
|
||||
private stateful function refund() : bool =
|
||||
if(Contract.balance > 0)
|
||||
switch(state.buyer)
|
||||
Some(buyer) =>
|
||||
Chain.spend(buyer, Contract.balance)
|
||||
true
|
||||
None =>
|
||||
false
|
||||
else
|
||||
false
|
||||
|
||||
private stateful function dead_claim() : bool =
|
||||
if(Contract.balance > 0)
|
||||
Chain.spend(state.tsuriai, Contract.balance)
|
||||
true
|
||||
else
|
||||
false
|
||||
@@ -0,0 +1,354 @@
|
||||
include "List.aes"
|
||||
include "Pair.aes"
|
||||
namespace Miner =
|
||||
type package_code = string
|
||||
|
||||
record package = {
|
||||
daily_cap : int,
|
||||
price : int}
|
||||
|
||||
record aggregated_package = {
|
||||
daily_cap : int,
|
||||
count : int}
|
||||
|
||||
record worker = {
|
||||
daily_cap : int,
|
||||
can_withdraw_payout : bool,
|
||||
packages : map(package_code, aggregated_package),
|
||||
joined_pool_tmst : int}
|
||||
|
||||
record transfer_packs =
|
||||
{
|
||||
worker : address,
|
||||
packages_to_move : list(string * int),
|
||||
new_address : address
|
||||
}
|
||||
|
||||
datatype approvable_action = Transfer(transfer_packs)
|
||||
|
||||
function new_package(price : int, cap : int) : package =
|
||||
{daily_cap = cap,
|
||||
price = price}
|
||||
|
||||
function claim(ps : list(package_code * (package * int)), joined_tmst : int) : worker =
|
||||
let daily_cap = daily_cap_from_packs_list(ps)
|
||||
let packs : map(package_code, aggregated_package) =
|
||||
List.foldl(
|
||||
(accum, t) =>
|
||||
let pack_id = Pair.fst(t)
|
||||
let (pack, cnt) = Pair.snd(t)
|
||||
let val =
|
||||
switch(Map.lookup(pack_id, accum))
|
||||
None => {daily_cap = pack.daily_cap, count = cnt}
|
||||
Some(v) => v{count = v.count + cnt}
|
||||
accum{[pack_id] = val},
|
||||
{},
|
||||
ps)
|
||||
{daily_cap = daily_cap,
|
||||
can_withdraw_payout = false,
|
||||
packages = packs,
|
||||
joined_pool_tmst = joined_tmst}
|
||||
|
||||
function split_packages(w : worker, split : transfer_packs) : worker * worker =
|
||||
let (packages_left, packages_collected) =
|
||||
List.foldl(
|
||||
(accum, p) =>
|
||||
let accum_left = Pair.fst(accum)
|
||||
let accum_collected = Pair.snd(accum)
|
||||
let code = Pair.fst(p)
|
||||
let count = Pair.snd(p)
|
||||
switch(Map.lookup(code, accum_left))
|
||||
None => abort("Does not own enough packages")
|
||||
Some(owned_packs) =>
|
||||
let left_packs = owned_packs.count - count
|
||||
require(left_packs > -1, "Does not own enough packages")
|
||||
(accum_left{[code] = owned_packs{count = left_packs}}, (code, owned_packs{count = count}) :: accum_collected),
|
||||
(w.packages, []),
|
||||
split.packages_to_move)
|
||||
let daily_cap_delta = List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, packages_collected))
|
||||
let new_w = {daily_cap = daily_cap_delta,
|
||||
can_withdraw_payout = false,
|
||||
packages = Map.from_list(packages_collected),
|
||||
joined_pool_tmst = Chain.block_height}
|
||||
require(Map.size(new_w.packages) == List.length(packages_collected), "Do not split counts of the same package code")
|
||||
(w{daily_cap = w.daily_cap - daily_cap_delta, packages = packages_left}, new_w)
|
||||
|
||||
function merge_workers(w1 : worker, w2: worker) =
|
||||
let packages =
|
||||
List.foldl(
|
||||
(accum, t) =>
|
||||
let code = Pair.fst(t)
|
||||
let aggr_pack = Pair.snd(t)
|
||||
let updated_pack =
|
||||
switch(Map.lookup(code, accum))
|
||||
None => aggr_pack
|
||||
Some(p) => p{count = p.count + aggr_pack.count}
|
||||
accum{[code] = updated_pack},
|
||||
w1.packages,
|
||||
Map.to_list(w2.packages))
|
||||
// if one of them is allowed to withdraw, so is the resulting new account
|
||||
let oldest_tmst =
|
||||
switch(w1.joined_pool_tmst < w2.joined_pool_tmst)
|
||||
true => w1.joined_pool_tmst
|
||||
false => w2.joined_pool_tmst
|
||||
let can_withdraw_payout = w1.can_withdraw_payout || w2.can_withdraw_payout
|
||||
let daily_cap = daily_cap_from_packages(packages)
|
||||
{daily_cap = daily_cap,
|
||||
can_withdraw_payout = can_withdraw_payout,
|
||||
packages = packages,
|
||||
joined_pool_tmst = oldest_tmst}
|
||||
|
||||
function daily_cap_from_packs_list(ps : list(package_code * (package * int))) =
|
||||
List.sum(List.map((t) => Pair.fst(Pair.snd(t)).daily_cap * Pair.snd(Pair.snd(t)), ps))
|
||||
|
||||
function daily_cap_from_packages(ps : map(package_code, aggregated_package)) =
|
||||
List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, Map.to_list(ps)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
contract interface Data =
|
||||
stateful entrypoint set_pool : (address) => unit
|
||||
stateful entrypoint add : (address, Miner.worker) => unit
|
||||
stateful entrypoint remove : (address) => unit
|
||||
payable stateful entrypoint give_rewards : (list(address * int)) => unit
|
||||
entrypoint balance : (address) => int
|
||||
entrypoint assert_is_payable : (address) => unit
|
||||
stateful entrypoint payout : () => unit
|
||||
stateful entrypoint payout_without_payable_check : (address) => unit
|
||||
entrypoint all_balances : () => list(address * int)
|
||||
entrypoint all_daily_caps : () => list(address * int)
|
||||
entrypoint all : () => list(address)
|
||||
entrypoint all_detailed : () => map(address, Miner.worker)
|
||||
entrypoint member : (address) => bool
|
||||
entrypoint get : (address) => Miner.worker
|
||||
stateful entrypoint rename : (address, address) => unit
|
||||
stateful entrypoint make_payable : (address) => unit
|
||||
stateful entrypoint make_non_payable : (address) => unit
|
||||
stateful entrypoint split_packages : (Miner.transfer_packs) => unit
|
||||
entrypoint all_balances_and_daily_caps : () => list(address * int * int)
|
||||
|
||||
contract interface Pool =
|
||||
entrypoint leader : () => address
|
||||
stateful entrypoint enroll : (address, Miner.worker) => unit
|
||||
entrypoint member : (address) => bool
|
||||
stateful entrypoint remove : (address) => unit
|
||||
entrypoint get : (address) => Miner.worker
|
||||
stateful entrypoint set_locked : (bool) => unit
|
||||
stateful entrypoint set_leader : (address) => unit
|
||||
entrypoint can_be_destroyed : () => bool
|
||||
entrypoint info : () => address * string * address * string * string * string * string * list(string)
|
||||
stateful entrypoint make_payable : (address) => unit
|
||||
stateful entrypoint make_non_payable : (address) => unit
|
||||
entrypoint assert_worker_is_payable : (address) => unit
|
||||
stateful entrypoint force_payout : (address) => unit
|
||||
stateful entrypoint change_worker_address : (address, address) => unit
|
||||
entrypoint balance : (address) => int
|
||||
stateful entrypoint set_data_ct : (Data) => unit
|
||||
stateful entrypoint move_data_and_coins_to_new_pool : (Pool) => Data
|
||||
payable entrypoint receive_coins : () => unit
|
||||
stateful entrypoint evacuate_coins : (int, address) => unit
|
||||
stateful entrypoint split_packages : (Miner.transfer_packs) => unit
|
||||
|
||||
|
||||
include "Set.aes"
|
||||
include "List.aes"
|
||||
include "Pair.aes"
|
||||
|
||||
main contract PoolInstance:Pool =
|
||||
datatype pool_status = OPEN | LOCKED | MIGRATED
|
||||
|
||||
datatype event
|
||||
= Enroll(address)
|
||||
| Remove(address)
|
||||
|
||||
record state =
|
||||
{
|
||||
status : pool_status,
|
||||
leader : address,
|
||||
main_contract : address,
|
||||
connect_addresses : Set.set(string), // IP addresses and ports
|
||||
leader_name : string,
|
||||
leader_url: string,
|
||||
leader_avatar_url : string,
|
||||
leader_description : string,
|
||||
data_ct : Data
|
||||
}
|
||||
|
||||
entrypoint init(main_contract : address, leader : address, data_ct : Data) =
|
||||
{status = OPEN,
|
||||
leader = leader,
|
||||
main_contract = main_contract,
|
||||
connect_addresses = Set.new(),
|
||||
leader_name = "",
|
||||
leader_url = "",
|
||||
leader_avatar_url = "",
|
||||
leader_description = "",
|
||||
data_ct = data_ct}
|
||||
|
||||
entrypoint status() =
|
||||
switch(state.status)
|
||||
OPEN => "open"
|
||||
LOCKED => "locked"
|
||||
MIGRATED => "migrated"
|
||||
|
||||
entrypoint info() =
|
||||
(Contract.address, status(), state.leader, state.leader_name, state.leader_url, state.leader_avatar_url, state.leader_description, Set.to_list(state.connect_addresses))
|
||||
|
||||
entrypoint data_contract() =
|
||||
state.data_ct
|
||||
|
||||
entrypoint main_contract() =
|
||||
state.main_contract
|
||||
|
||||
entrypoint can_be_destroyed() : bool =
|
||||
(empty() && state.status == LOCKED && Contract.balance == 0) || state.status == MIGRATED
|
||||
|
||||
entrypoint leader() =
|
||||
state.leader
|
||||
|
||||
entrypoint miners() =
|
||||
state.data_ct.all()
|
||||
|
||||
entrypoint miners_detailed() =
|
||||
state.data_ct.all_detailed()
|
||||
|
||||
entrypoint miner_balances() : list(address * int) =
|
||||
state.data_ct.all_balances()
|
||||
|
||||
entrypoint miner_daily_caps() : list(address * int) =
|
||||
state.data_ct.all_daily_caps()
|
||||
|
||||
entrypoint miner_balances_and_caps() : list(address * int * int) =
|
||||
state.data_ct.all_balances_and_daily_caps()
|
||||
|
||||
entrypoint empty() : bool =
|
||||
List.is_empty(miners())
|
||||
|
||||
stateful entrypoint enroll(worker_address : address, worker : Miner.worker) =
|
||||
assert_caller_is_main_contract()
|
||||
require(state.status == OPEN, "Pool is locked, can not join it")
|
||||
Chain.event(Enroll(worker_address))
|
||||
add_worker(worker_address, worker)
|
||||
|
||||
entrypoint member(worker_address : address) : bool =
|
||||
state.data_ct.member(worker_address)
|
||||
|
||||
|
||||
/* deletes a worker from the pool if present. Currently the accumulated coins remain in
|
||||
pool. */
|
||||
stateful entrypoint remove(worker_address : address) =
|
||||
assert_caller_is_main_contract()
|
||||
Chain.event(Remove(worker_address))
|
||||
state.data_ct.remove(worker_address)
|
||||
|
||||
entrypoint get(worker_address : address) =
|
||||
state.data_ct.get(worker_address)
|
||||
|
||||
// this can overwrite a MIGRATED state
|
||||
stateful entrypoint set_locked(val : bool) =
|
||||
assert_caller_is_main_contract()
|
||||
let s =
|
||||
switch(val)
|
||||
true => LOCKED
|
||||
false => OPEN
|
||||
put(state{status = s})
|
||||
|
||||
stateful entrypoint set_leader(new_leader : address) =
|
||||
assert_caller_is_main_contract()
|
||||
put(state{leader = new_leader})
|
||||
|
||||
stateful entrypoint add_connect_address(conn_address : string) =
|
||||
assert_leader()
|
||||
put(state{connect_addresses = Set.insert(conn_address, state.connect_addresses)})
|
||||
|
||||
stateful entrypoint rm_connect_address(conn_address : string) =
|
||||
assert_leader()
|
||||
put(state{connect_addresses = Set.delete(conn_address, state.connect_addresses)})
|
||||
|
||||
stateful entrypoint set_name(name : string) =
|
||||
assert_leader()
|
||||
put(state{leader_name = name})
|
||||
|
||||
stateful entrypoint set_url(url : string) =
|
||||
assert_leader()
|
||||
put(state{leader_url = url})
|
||||
|
||||
stateful entrypoint set_avatar_url(avatar_url : string) =
|
||||
assert_leader()
|
||||
put(state{leader_avatar_url = avatar_url})
|
||||
|
||||
stateful entrypoint set_description(description : string) =
|
||||
assert_leader()
|
||||
put(state{leader_description = description})
|
||||
|
||||
/* NB: does not take into account one's daily limits! */
|
||||
stateful entrypoint simply_reward_work(amounts : list(address * int), _ : string) =
|
||||
assert_leader()
|
||||
let total_reward = List.sum(List.map((t) => Pair.snd(t), amounts))
|
||||
require(Contract.balance >= total_reward, "Not enough GAJU for that reward")
|
||||
state.data_ct.give_rewards(amounts, value = total_reward)
|
||||
|
||||
payable entrypoint receive_coins() =
|
||||
()
|
||||
|
||||
// for Eureka
|
||||
entrypoint balance(addr : address) =
|
||||
state.data_ct.balance(addr)
|
||||
|
||||
entrypoint daily_cap(addr : address) =
|
||||
let worker = get(addr)
|
||||
worker.daily_cap
|
||||
|
||||
stateful entrypoint payout() : unit =
|
||||
state.data_ct.payout()
|
||||
|
||||
stateful entrypoint force_payout(worker_addr : address) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.payout_without_payable_check(worker_addr)
|
||||
|
||||
stateful entrypoint change_worker_address(old_addr : address, new_addr : address) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.rename(old_addr, new_addr)
|
||||
|
||||
stateful entrypoint evacuate_coins(amount : int, safeheaven : address) =
|
||||
assert_caller_is_main_contract()
|
||||
Chain.spend(safeheaven, amount)
|
||||
|
||||
stateful entrypoint make_payable(worker_address : address) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.make_payable(worker_address)
|
||||
|
||||
stateful entrypoint make_non_payable(worker_address : address) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.make_non_payable(worker_address)
|
||||
|
||||
stateful entrypoint set_data_ct(data_ct : Data) =
|
||||
assert_caller_is_main_contract()
|
||||
put(state{data_ct = data_ct})
|
||||
|
||||
stateful entrypoint move_data_and_coins_to_new_pool(new_pool : Pool) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.set_pool(new_pool.address)
|
||||
new_pool.receive_coins(value = Contract.balance)
|
||||
put(state{status = MIGRATED})
|
||||
state.data_ct
|
||||
|
||||
entrypoint assert_worker_is_payable(worker_address : address) =
|
||||
state.data_ct.assert_is_payable(worker_address)
|
||||
|
||||
stateful entrypoint split_packages(split : Miner.transfer_packs) =
|
||||
assert_caller_is_main_contract()
|
||||
state.data_ct.split_packages(split)
|
||||
|
||||
// private functions
|
||||
function assert_caller_is_main_contract() =
|
||||
require(Call.caller == state.main_contract, "Call it through the main pool contract")
|
||||
|
||||
function assert_leader() =
|
||||
require(Call.origin == state.leader, "Must be called by the leader")
|
||||
|
||||
stateful function add_worker(worker_address, worker) =
|
||||
state.data_ct.add(worker_address, worker)
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Example gym subscription contract
|
||||
*
|
||||
* Copyright (C) 2025, QPQ AG. All Rights Reserved.
|
||||
*
|
||||
* Owner can
|
||||
* - run "tick" to collect monthly fees -> spent to owner
|
||||
* - unsubscribe anyone
|
||||
*
|
||||
* Anyone can
|
||||
* - subscribe for a period of N months (debit system - all money paid up front)
|
||||
* - extend their subscription for K months
|
||||
* - cancel their subscription and withdraw any future money
|
||||
*
|
||||
* Owner must issue new contract if he wants to change monthly fee; otherwise
|
||||
* data structure is too fucking complicated.
|
||||
*/
|
||||
|
||||
include "List.aes"
|
||||
|
||||
payable contract Gym =
|
||||
type pucks = int
|
||||
type n_months = int
|
||||
record state = {owner : address,
|
||||
members : map(address, n_months),
|
||||
monthly_fee : pucks}
|
||||
|
||||
// SFP accessors
|
||||
entrypoint
|
||||
get_state : () => state
|
||||
get_state() = state
|
||||
stateful entrypoint
|
||||
put_state : state => unit
|
||||
put_state(s) = put(s)
|
||||
|
||||
entrypoint
|
||||
is_member : address => bool
|
||||
is_member(whomst) = Map.member(whomst, state.members)
|
||||
|
||||
entrypoint
|
||||
owner : () => address
|
||||
owner() = state.owner
|
||||
|
||||
entrypoint
|
||||
members : () => map(address, pucks)
|
||||
members() = state.members
|
||||
|
||||
entrypoint
|
||||
monthly_fee : () => pucks
|
||||
monthly_fee() = state.monthly_fee
|
||||
|
||||
|
||||
// actual things
|
||||
entrypoint
|
||||
init : (pucks) => state
|
||||
init(monthly_rate) =
|
||||
require(monthly_rate >= 0, "monthly rate must be non-negative")
|
||||
{owner = Call.caller,
|
||||
members = {},
|
||||
monthly_fee = monthly_rate}
|
||||
|
||||
//------------------------------------------------
|
||||
// OWNER ENTRYPOINTS: tick/ban
|
||||
//------------------------------------------------
|
||||
|
||||
stateful entrypoint
|
||||
tick : () => unit
|
||||
tick() | Call.caller != state.owner =
|
||||
abort("you are not allowed to do ticks!")
|
||||
tick() =
|
||||
// collect fees
|
||||
// 1 monthly charge per member
|
||||
let total_charge = state.monthly_fee * Map.size(state.members)
|
||||
Chain.spend(state.owner, total_charge)
|
||||
|
||||
// update members list (clean out everyone with just 1 month left)
|
||||
let old_members_list : list(address * n_months) = Map.to_list(state.members)
|
||||
let new_members_list : list(address * n_months) = List.foldl(deduct_month, [], old_members_list)
|
||||
let new_members : map(address, n_months) = Map.from_list(new_members_list)
|
||||
let new_state : state = state{members = new_members}
|
||||
put(new_state)
|
||||
|
||||
function
|
||||
deduct_month : (list(address*n_months), address*n_months) => list(address*n_months)
|
||||
// n>1 months left -> subtract a month but keep in membership list
|
||||
deduct_month(acc, (patron, months_left)) | months_left > 1 =
|
||||
(patron, months_left-1) :: acc
|
||||
// only 1 month left -> remove from membership list
|
||||
deduct_month(acc, (patron, 1)) =
|
||||
acc
|
||||
|
||||
stateful entrypoint
|
||||
ban : (address) => unit
|
||||
ban(_) | Call.caller != state.owner =
|
||||
abort("you are not allowed to ban people!")
|
||||
ban(patron) =
|
||||
refund(patron)
|
||||
|
||||
|
||||
//------------------------------------------------
|
||||
// PUBLIC ENTRYPOINTS: subscribe/cancel
|
||||
//------------------------------------------------
|
||||
|
||||
payable stateful entrypoint
|
||||
subscribe : (n_months) => unit
|
||||
subscribe(n) | n < 1 =
|
||||
abort("must subscribe for at least 1 month")
|
||||
subscribe(n) | Call.value < n*state.monthly_fee =
|
||||
abort("not enough money to subscribe for that many months!")
|
||||
subscribe(n) =
|
||||
let charge: pucks = n * state.monthly_fee
|
||||
// call will be successful
|
||||
// refund caller extra money
|
||||
let extra_money: pucks = Call.value - charge
|
||||
Chain.spend(Call.caller, extra_money)
|
||||
// update membership
|
||||
let old_months : n_months = Map.lookup_default(Call.caller, state.members, 0)
|
||||
let new_months : n_months = old_months + n
|
||||
let new_members : map(address, n_months) = state.members{[Call.caller] = new_months}
|
||||
let new_state : state = state{members = new_members}
|
||||
put(new_state)
|
||||
|
||||
stateful entrypoint
|
||||
cancel : () => unit
|
||||
cancel() =
|
||||
refund(Call.caller)
|
||||
|
||||
//------------------------------------------------
|
||||
// INTERNAL: refund people
|
||||
//------------------------------------------------
|
||||
|
||||
stateful function
|
||||
refund : (address) => unit
|
||||
refund(patron) =
|
||||
let patron_months : n_months =
|
||||
switch (Map.lookup(patron, state.members))
|
||||
None => abort("already not a member!")
|
||||
Some(remaining_months) => remaining_months
|
||||
let patron_balance : pucks = patron_months * state.monthly_fee
|
||||
// update membership
|
||||
let new_members : map(address, n_months) = Map.delete(patron, state.members)
|
||||
let new_state : state = state{members = new_members}
|
||||
put(new_state)
|
||||
// refund balance to patron
|
||||
Chain.spend(patron, patron_balance)
|
||||
@@ -0,0 +1,10 @@
|
||||
// Hello World Contract
|
||||
// Copyright (c) 2025 QPQ AG
|
||||
|
||||
contract Hello =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
()
|
||||
|
||||
entrypoint hello(): string =
|
||||
"hello, world"
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_ABC'x'
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_ABC0DEF
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_AB0I'x'bar
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_AB0xDEF
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_ABClet
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_foolbar
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_ABC'foo
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ct_fooObar
|
||||
()
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
let _ = #deadbeef
|
||||
42
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
let _ = '\xab'
|
||||
42
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
let _ = '\x{ff}'
|
||||
42
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : string =
|
||||
"hello
|
||||
world"
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
type state = unit
|
||||
entrypoint init() : state =
|
||||
() /*
|
||||
@@ -0,0 +1,3 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
1 + /* unclosed
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
/* outer /* inner */ outer still open
|
||||
42
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
/* unclosed block comment
|
||||
42
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : string =
|
||||
"hello\
|
||||
world"
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Test =
|
||||
type state = unit
|
||||
entrypoint init() : state =
|
||||
let _ = 'é'
|
||||
()
|
||||
@@ -0,0 +1,4 @@
|
||||
contract Test =
|
||||
entrypoint main() : int =
|
||||
let c = 'Ā'
|
||||
Char.to_int(c)
|
||||
@@ -0,0 +1,3 @@
|
||||
contract Test =
|
||||
entrypoint greet() : string =
|
||||
"∑∆∏"
|
||||
@@ -0,0 +1,3 @@
|
||||
contract Col2Byte =
|
||||
entrypoint greet() : string =
|
||||
"Héllo" + " world"
|
||||
@@ -0,0 +1,3 @@
|
||||
contract Col3Byte =
|
||||
entrypoint price() : string =
|
||||
"Price: €10" + " incl."
|
||||
@@ -0,0 +1,3 @@
|
||||
contract Col4Byte =
|
||||
entrypoint celebrate() : string =
|
||||
"Hello 🎉" + " world"
|
||||
@@ -0,0 +1,3 @@
|
||||
contract ColBcom =
|
||||
entrypoint compute(x : int) : int =
|
||||
/* résultat */ x + 1
|
||||
@@ -0,0 +1,3 @@
|
||||
contract ColMulti =
|
||||
entrypoint unicode_str() : string =
|
||||
"Ünïcödé" + " ok"
|
||||
@@ -0,0 +1,8 @@
|
||||
main contract Foo =
|
||||
type state = unit
|
||||
|
||||
entrypoint init(): state =
|
||||
()
|
||||
|
||||
entrypoint foo'(): unit = ()
|
||||
entrypoint foo'bar'(): unit = ()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = ak_ABC123
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = sg_foo_bar
|
||||
()
|
||||
@@ -0,0 +1,5 @@
|
||||
contract Foo =
|
||||
type state = unit
|
||||
entrypoint init(): state =
|
||||
let _ = sg_GHI
|
||||
()
|
||||
@@ -0,0 +1,6 @@
|
||||
main contract Foo =
|
||||
type state = unit
|
||||
type foo = bar // id
|
||||
type foo = Bar.baz // qid
|
||||
type foo = 'bar // tvar
|
||||
type foo = () => bar
|
||||
@@ -0,0 +1,273 @@
|
||||
% @doc experiment centering around the file syntax node using ntree approach
|
||||
-module(gsc_test_file).
|
||||
|
||||
-export([
|
||||
main/0
|
||||
]).
|
||||
|
||||
-include("$gsc_include/gsc.hrl").
|
||||
|
||||
|
||||
-record(ct,
|
||||
{payable = none :: none | false | {true, tk()},
|
||||
main = none :: none | false | {true, tk()},
|
||||
contract = none :: none | tk(),
|
||||
con = none :: none | tk(),
|
||||
impls = none :: none | [tk()],
|
||||
eq = none :: none | tk()}).
|
||||
|
||||
-type meta() :: #ct{}.
|
||||
|
||||
-record(decl_type,
|
||||
{type = none :: none | tk(),
|
||||
id = none :: none | tk(),
|
||||
params = none :: none | [tk()],
|
||||
eq = none :: none | tk()}).
|
||||
|
||||
-type decl_meta() :: #decl_type{}.
|
||||
|
||||
|
||||
-type ast_meta() :: file
|
||||
| meta()
|
||||
| decl_meta()
|
||||
| nyi
|
||||
| {nyi, any()}
|
||||
.
|
||||
|
||||
|
||||
-type target()
|
||||
:: ct
|
||||
| iface
|
||||
| ns
|
||||
| pragma
|
||||
| include
|
||||
| using
|
||||
.
|
||||
|
||||
-type s2t_target()
|
||||
:: file
|
||||
| top_decl
|
||||
| target()
|
||||
| nyi
|
||||
| {nyi, any()}
|
||||
.
|
||||
|
||||
-type s2f_target()
|
||||
:: {block_of, s2t_target()}
|
||||
.
|
||||
|
||||
|
||||
-type ast() :: ntree(ast_meta(), tk()).
|
||||
-type asf() :: nforest(ast_meta(), tk()).
|
||||
|
||||
main() ->
|
||||
HelloN = "hello.aes",
|
||||
HelloP = ts_utils:ct_file_abspath(HelloN),
|
||||
{ok, HelloS} = file:read_file(HelloP),
|
||||
S0 = gsc:unsafe_signal_from_file(HelloP),
|
||||
T1 = s2t(file, S0),
|
||||
io:format("hello.aes:~n", []),
|
||||
io:format("```~n", []),
|
||||
io:format("~ts", [HelloS]),
|
||||
io:format("```~n~n", []),
|
||||
io:format("AST: ~tp~n", [T1]),
|
||||
ok.
|
||||
|
||||
% // Hello World Contract
|
||||
% // Copyright (c) 2025 QPQ AG
|
||||
%
|
||||
% contract Hello =
|
||||
% type state = unit
|
||||
% entrypoint init(): state =
|
||||
% ()
|
||||
%
|
||||
% entrypoint hello(): string =
|
||||
% "hello, world"
|
||||
|
||||
-spec s2t(ParseTarget, Signal) -> AST when
|
||||
ParseTarget :: file,
|
||||
Signal :: [tk()],
|
||||
AST :: ast().
|
||||
|
||||
% File ::= Block(TopDecl)
|
||||
s2t(file, Signal) ->
|
||||
case Signal of
|
||||
[] -> error(empty_file);
|
||||
_ -> {ns, file, s2f({block_of, top_decl}, Signal)}
|
||||
end;
|
||||
% TopDecl ::= ['payable'] ['main'] 'contract' Con [Implement] '=' Block(Decl)
|
||||
% | ['payable'] 'contract' 'interface' Con [Implement] '=' Block(Decl)
|
||||
% | 'namespace' Con '=' Block(Decl)
|
||||
% | '@compiler' PragmaOp Version
|
||||
% | 'include' String
|
||||
% | Using
|
||||
s2t(top_decl, Signal) ->
|
||||
NewTarget =
|
||||
case gsc_tokens:strings(3, Signal) of
|
||||
["payable", "contract", "interface"] -> iface;
|
||||
["contract", "interface" | _] -> iface;
|
||||
["payable", "main", "contract"] -> ct;
|
||||
["payable", "contract" | _] -> ct;
|
||||
["contract" | _] -> ct;
|
||||
["namespace" | _] -> namespace;
|
||||
["@compiler" | _] -> pragma;
|
||||
["include" | _] -> include;
|
||||
["using" | _] -> using
|
||||
end,
|
||||
s2t(NewTarget, Signal);
|
||||
% ['payable'] ['main'] 'contract' Con [Implement] '=' Block(Decl)
|
||||
s2t(ct, S0) ->
|
||||
{slurp, CtMeta, S1} = s2s_slurp_meta(#ct{}, S0),
|
||||
{ns, CtMeta, s2f({block_of, decl}, S1)};
|
||||
% Decl ::= 'type' Id ['(' TVar* ')'] '=' TypeAlias
|
||||
% | 'record' Id ['(' TVar* ')'] '=' RecordType
|
||||
% | 'datatype' Id ['(' TVar* ')'] '=' DataType
|
||||
% | 'let' Id [':' Type] '=' Expr
|
||||
% | (EModifier* 'entrypoint' | FModifier* 'function') Block(FunDecl)
|
||||
% | Using
|
||||
s2t(decl, S0) ->
|
||||
NewTarget =
|
||||
case gsc_tokens:strings(3, S0) of
|
||||
["type" | _] -> decl_type;
|
||||
["record" | _] -> decl_record;
|
||||
["datatype" | _] -> decl_datatype;
|
||||
["let" | _] -> decl_let;
|
||||
Pfx3 ->
|
||||
IsEp = lists:member("entrypoint", Pfx3),
|
||||
IsFn = lists:member("function", Pfx3),
|
||||
if
|
||||
IsEp -> decl_entrypoint;
|
||||
IsFn -> decl_function;
|
||||
true -> error({bad_decl, S0})
|
||||
end
|
||||
end,
|
||||
s2t(NewTarget, S0);
|
||||
% 'type' Id ['(' TVar* ')'] '=' TypeAlias
|
||||
s2t(decl_type, S0) ->
|
||||
{slurp, Meta, S1} = s2s_slurp_meta(#decl_type{}, S0),
|
||||
{ns, Meta, s2t(type, S1)};
|
||||
s2t(nyi, Signal) ->
|
||||
{ns, nyi, Signal};
|
||||
s2t(NYI = {nyi, _}, Signal) ->
|
||||
{ns, NYI, Signal};
|
||||
s2t(NYI, Signal) ->
|
||||
{ns, {nyi, NYI}, Signal}.
|
||||
|
||||
|
||||
|
||||
-spec s2f(ForestTarget, Signal) -> Forest when
|
||||
ForestTarget :: s2f_target(),
|
||||
Signal :: [tk()],
|
||||
Forest :: asf().
|
||||
|
||||
s2f({block_of, TreeTarget}, S0) ->
|
||||
{gulp, Items} = gsc_signal:gulp_block_items(S0),
|
||||
[s2t(TreeTarget, I) || I <- Items].
|
||||
|
||||
|
||||
-spec s2s_slurp_meta(InitMeta, Signal) -> Result when
|
||||
InitMeta :: Meta,
|
||||
Signal :: [tk()],
|
||||
Result :: {slurp, Meta, NewSignal},
|
||||
Meta :: ast_meta(),
|
||||
NewSignal :: Signal.
|
||||
|
||||
s2s_slurp_meta(M = #ct{}, S) ->
|
||||
s2s_sm_ct(M, S);
|
||||
s2s_slurp_meta(M = #decl_type{}, S) ->
|
||||
s2s_sm_decl_type(M, S);
|
||||
s2s_slurp_meta(M, S) ->
|
||||
error({s2s_slurp_meta, M, S}).
|
||||
|
||||
|
||||
s2s_sm_ct(Ct = #ct{payable = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "payable"} = T0 | S1] ->
|
||||
s2s_sm_ct(Ct#ct{payable = {true, T0}}, S1);
|
||||
_ ->
|
||||
s2s_sm_ct(Ct#ct{payable = false}, S0)
|
||||
end;
|
||||
s2s_sm_ct(Ct = #ct{main = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "main"} = T0 | S1] ->
|
||||
s2s_sm_ct(Ct#ct{main = {true, T0}}, S1);
|
||||
_ ->
|
||||
s2s_sm_ct(Ct#ct{main = false}, S0)
|
||||
end;
|
||||
s2s_sm_ct(Ct = #ct{contract = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "contract"} = T0 | S1] ->
|
||||
s2s_sm_ct(Ct#ct{contract = T0}, S1);
|
||||
_ ->
|
||||
error({no_kwd_contract, Ct, S0})
|
||||
end;
|
||||
s2s_sm_ct(Ct = #ct{con = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{shape = con} = T0 | S1] ->
|
||||
s2s_sm_ct(Ct#ct{con = T0}, S1);
|
||||
_ ->
|
||||
error({no_contract_name, Ct, S0})
|
||||
end;
|
||||
s2s_sm_ct(Ct = #ct{impls = none}, S0) ->
|
||||
case gsc_tokens:strings(1, S0) of
|
||||
[":"] ->
|
||||
{slurp, Impls, S1} = s2f_slurp_impls(S0),
|
||||
s2s_sm_ct(Ct#ct{impls = Impls}, S1);
|
||||
_ ->
|
||||
s2s_sm_ct(Ct#ct{impls = []}, S0)
|
||||
end;
|
||||
s2s_sm_ct(Ct = #ct{eq = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "="} = T0 | S1] ->
|
||||
s2s_sm_ct(Ct#ct{eq = T0}, S1);
|
||||
_ ->
|
||||
error({no_equal_sign, Ct, S0})
|
||||
end;
|
||||
s2s_sm_ct(Ct, S0) ->
|
||||
{slurp, Ct, S0}.
|
||||
|
||||
s2f_slurp_impls([#tk{str = ":"}, #tk{shape = con} = I0 | S0]) ->
|
||||
s2f_slurp_impls([I0], S0).
|
||||
|
||||
s2f_slurp_impls(Stk, [#tk{str = ","}, #tk{shape = con} = I0 | S0]) ->
|
||||
s2f_slurp_impls([I0 | Stk], S0);
|
||||
s2f_slurp_impls(Stk, S0) ->
|
||||
{slurp, lists:reverse(Stk), S0}.
|
||||
|
||||
|
||||
%-record(decl_type,
|
||||
% {type = none :: none | tk(),
|
||||
% id = none :: none | tk(),
|
||||
% params = none :: none | [tk()],
|
||||
% eq = none :: none | tk()}).
|
||||
|
||||
s2s_sm_decl_type(M = #decl_type{type = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "type"} = T0 | S1] ->
|
||||
s2s_sm_decl_type(M#decl_type{type = T0}, S1);
|
||||
_ ->
|
||||
error({no_kwd_type, S0})
|
||||
end;
|
||||
s2s_sm_decl_type(M = #decl_type{id = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{shape = id} = T0 | S1] ->
|
||||
s2s_sm_decl_type(M#decl_type{id = T0}, S1);
|
||||
_ ->
|
||||
error({no_type_id, S0})
|
||||
end;
|
||||
s2s_sm_decl_type(M = #decl_type{params = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "("} = T0 | _] ->
|
||||
error({fixme, parens_bad});
|
||||
_ ->
|
||||
s2s_sm_decl_type(M#decl_type{params = []}, S0)
|
||||
end;
|
||||
s2s_sm_decl_type(M = #decl_type{eq = none}, S0) ->
|
||||
case S0 of
|
||||
[#tk{str = "="} = T0 | S1] ->
|
||||
s2s_sm_decl_type(M#decl_type{eq = T0}, S1);
|
||||
_ ->
|
||||
error({no_equal_sign, S0})
|
||||
end;
|
||||
s2s_sm_decl_type(M, S0) ->
|
||||
{slurp, M, S0}.
|
||||
@@ -0,0 +1,144 @@
|
||||
-module(gsc_test_ntree).
|
||||
|
||||
-export([
|
||||
main/0
|
||||
]).
|
||||
|
||||
-include("$gsc_include/gsc.hrl").
|
||||
|
||||
% just parsing type expressions right now, so only need
|
||||
% to worry about round parens
|
||||
%
|
||||
% none is to indicate general-purpose grouping, for
|
||||
% e.g. LHS/RHS of an op
|
||||
-type syntax_meta()
|
||||
:: {op, tk()}
|
||||
| op_arg
|
||||
| {parens, Open :: tk(), Close :: tk()}
|
||||
.
|
||||
|
||||
-type ast() :: ntree(syntax_meta(), tk()).
|
||||
-type asf() :: nforest(syntax_meta(), tk()).
|
||||
-type asts() :: asf().
|
||||
|
||||
|
||||
main() ->
|
||||
x00(),
|
||||
ok.
|
||||
|
||||
% x00 = example00
|
||||
x00() ->
|
||||
io:format("Example 00:~n", []),
|
||||
io:format(" SrcStr = ~p~n", [x00_src()]),
|
||||
io:format(" Tokens = ~p~n", [x00_tks()]),
|
||||
io:format(" Signal = ~p~n", [x00_sgl()]),
|
||||
io:format(" Forest = ~p~n", [x00_fst()]),
|
||||
ok.
|
||||
|
||||
% sample type expr, tokens, signal
|
||||
x00_src() -> "(foo => (bar) * baz)".
|
||||
x00_tks() -> gsc:unsafe_tokens_from_string(x00_src()).
|
||||
x00_sgl() -> gsc:filter_signal(x00_tks()).
|
||||
x00_fst() -> parse(x00_sgl()).
|
||||
|
||||
|
||||
-spec parse(Signal) -> ASF when
|
||||
Signal :: [tk()],
|
||||
ASF :: asf().
|
||||
|
||||
parse(Signal) ->
|
||||
% key insight here is our signal is already a
|
||||
% forest, assuming the leaf type is `tk()`.
|
||||
%
|
||||
% our parser is a sequence of forest-to-forest
|
||||
% transformers.
|
||||
%
|
||||
% at the end we should end up with just one tree (i
|
||||
% think)?
|
||||
F0 = Signal,
|
||||
F1 = f2f_parens(F0),
|
||||
F2 = f2f_op("=>", F1),
|
||||
F3 = f2f_op("*", F2),
|
||||
Result = F2,
|
||||
Result.
|
||||
|
||||
|
||||
f2f_op(OpStr, Fst) ->
|
||||
f2f_op(OpStr, [], Fst).
|
||||
|
||||
|
||||
% never saw the op
|
||||
f2f_op(_opstr, Stk, []) ->
|
||||
lists:reverse(Stk);
|
||||
% see op
|
||||
f2f_op(OpStr, LhsStk, [#tk{str = OpStr} = OpTk | Rest]) ->
|
||||
Lhf = lists:reverse(LhsStk),
|
||||
Rhf = f2f_op(OpStr, Rest),
|
||||
Lht = #ns{meta = op_arg, kids = Lhf},
|
||||
Rht = #ns{meta = op_arg, kids = Rhf},
|
||||
ResultT = #ns{meta = {op, OpTk},
|
||||
kids = [Lht, Rht]},
|
||||
ResultF = [ResultT],
|
||||
ResultF;
|
||||
% see stem, descend
|
||||
f2f_op(OpStr, LhsStk, [Ns = #ns{kids = NsKids} | Rest]) ->
|
||||
NewNsKids = f2f_op(OpStr, NsKids),
|
||||
NewNs = Ns#ns{kids = NewNsKids},
|
||||
NewStk = [NewNs | LhsStk],
|
||||
f2f_op(OpStr, NewStk, Rest);
|
||||
% see leaf, just add
|
||||
f2f_op(OpStr, Stk, [L | Rest]) ->
|
||||
f2f_op(OpStr, [L | Stk], Rest).
|
||||
|
||||
|
||||
-spec f2f_parens(Forest) -> NewForest when
|
||||
Forest :: asts(),
|
||||
NewForest :: Forest.
|
||||
% @doc
|
||||
% recursive parens decomposition
|
||||
%
|
||||
% the input here is the flat list of tokens. here we
|
||||
% basically replace the string of tokens between `(`
|
||||
% and `)` with a single tree
|
||||
%
|
||||
% interesting quirk is that this doesn't error on too
|
||||
% many close parens, only too many open parens
|
||||
|
||||
f2f_parens(Fst) ->
|
||||
f2f_parens([], Fst).
|
||||
|
||||
% done
|
||||
f2f_parens(Stk, []) ->
|
||||
lists:reverse(Stk);
|
||||
% crawl down the forest and scan for open parens
|
||||
% open paren, we descend
|
||||
f2f_parens(Stk, [#tk{str = "("} = TkOpen | Rest0]) ->
|
||||
InitMeta = {parens, TkOpen, none},
|
||||
{slurp, PStem, Rest1} = slurp_pstem(InitMeta, [], Rest0),
|
||||
NewStk = [PStem | Stk],
|
||||
f2f_parens(NewStk, Rest1);
|
||||
% something else, we continue
|
||||
f2f_parens(Stk, [Tree | Rest]) ->
|
||||
f2f_parens([Tree | Stk], Rest).
|
||||
|
||||
|
||||
|
||||
% ran out of tokens before close paren
|
||||
slurp_pstem({parens, TkOpen, none}, Stk, []) ->
|
||||
error({no_close_for, TkOpen, Stk});
|
||||
% hit close paren, we done
|
||||
slurp_pstem({parens, TkOpen, none}, Stk, [TkClose = #tk{str = ")"} | Rest]) ->
|
||||
FinalMeta = {parens, TkOpen, TkClose},
|
||||
Midsection = lists:reverse(Stk),
|
||||
FinalTree = #ns{meta = FinalMeta,
|
||||
kids = Midsection},
|
||||
{slurp, FinalTree, Rest};
|
||||
% hit open paren, we recurse
|
||||
slurp_pstem(AccMeta, Stk, [TkOpen_II = #tk{str = "("} | Rest0]) ->
|
||||
InitMeta_II = {parens, TkOpen_II, none},
|
||||
{slurp, PStem_II, Rest1} = slurp_pstem(InitMeta_II, [], Rest0),
|
||||
NewStk = [PStem_II | Stk],
|
||||
slurp_pstem(AccMeta, NewStk, Rest1);
|
||||
% hit something else, we move along
|
||||
slurp_pstem(AccMeta, Stk, [Tree | Rest]) ->
|
||||
slurp_pstem(AccMeta, [Tree | Stk], Rest).
|
||||
@@ -0,0 +1,165 @@
|
||||
% gsc tokenizer tests
|
||||
-module(gsc_test_tokens).
|
||||
|
||||
-export([
|
||||
main/0, ct_dir/0
|
||||
%tokens_match/1
|
||||
]).
|
||||
-include("$gsc_include/gsc.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
main() ->
|
||||
%io:format("~p~n", [div_files()]),
|
||||
%io:format("MAINNNNN!~n", []),
|
||||
eunit:test(?MODULE, [verbose]).
|
||||
%eunit:test(?MODULE).
|
||||
|
||||
|
||||
% directory containing the tests for the tokenizer
|
||||
ct_dir() ->
|
||||
zx_daemon:get_home() ++ "/ct".
|
||||
|
||||
agreement_tests_dir() ->
|
||||
ct_dir() ++ "/tokenizers_agree".
|
||||
|
||||
|
||||
% the divergences claude found between gsc tokenizer and so tokenizer
|
||||
%
|
||||
% mostly stupid corner cases like a string crossing a line boundary
|
||||
% or unterminated block comment
|
||||
%
|
||||
% divergence files: "divergence" means so_scan disagrees with
|
||||
% gsc_so_scan in one of the following ways:
|
||||
%
|
||||
% - one succeeds when the other errors
|
||||
% - disagree on success case
|
||||
%
|
||||
% making errors agree on two programs that work differently is a
|
||||
% fool's errand
|
||||
div_files() ->
|
||||
ContractsDir = agreement_tests_dir(),
|
||||
% this is the equivalent of ls
|
||||
% just has filenames, no /path/to/ prefix
|
||||
{ok, Files} = file:list_dir(ContractsDir),
|
||||
% originally i was a retard and didn't read the eunit
|
||||
% documentation, so if any one test failed, the entire test suite
|
||||
% would crash with no information regarding what happened
|
||||
%
|
||||
% so this was a hack to only run div01-div05 but not div06:
|
||||
%
|
||||
% % hack to fix one broken test at a time
|
||||
% (FileName = "div0" ++ [Digit | _]) when Digit =< $9 ->
|
||||
% FilePath = ct_dir() ++ "/" ++ FileName,
|
||||
% {true, {FileName, FilePath}}
|
||||
% (_) -> false
|
||||
%
|
||||
% Once i read the eunit docs and learned about test generators, I
|
||||
% realized I could have only the failed test chimp out. what a
|
||||
% concept.
|
||||
%
|
||||
% i also realized that printing the full filepath was a waste, so
|
||||
% instead the test should know about the FileName (foo.bar) and the
|
||||
% FilePath (/path/to/foo.bar).
|
||||
%
|
||||
% then i decided to start writing my own test contracts instead
|
||||
% of having claude do it and i rean into the issue of vim swap
|
||||
% files not lexing properly because they're not unicode
|
||||
IsDivCt =
|
||||
fun(FileName) ->
|
||||
% need to filter out vim swap files
|
||||
% originally was false-matching on ([$. | _])
|
||||
% like a man
|
||||
%
|
||||
% god this feels like putting my balls in a little tiny
|
||||
% guillotine (even the guillotine is emasculating) but
|
||||
% claude suggested this and i mean it's kind of the
|
||||
% most idiomatic and like straightforward. most
|
||||
% importantly it's declarative
|
||||
%
|
||||
% god i feel so defeated
|
||||
case filename:extension(FileName) of
|
||||
".aes" ->
|
||||
FilePath = ContractsDir ++ "/" ++ FileName,
|
||||
{true, {FileName, FilePath}};
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
lists:sort(lists:filtermap(IsDivCt, Files)).
|
||||
|
||||
|
||||
%div_file_names() -> [N || {N, _} <- div_files()].
|
||||
%div_file_paths() -> [P || {_, P} <- div_files()].
|
||||
|
||||
tokstr_concat_test_() ->
|
||||
% future proofing
|
||||
ConcatTestFiles
|
||||
= lists:flatten([
|
||||
div_files()
|
||||
]),
|
||||
% exclude the contracts with like unterminated block comments
|
||||
% where they don't tokenize properly
|
||||
NonStupidFiles =
|
||||
lists:filter(
|
||||
fun
|
||||
({"div05_bcom_eof.aes", _}) -> false;
|
||||
({"div06_bcom_in_expr.aes", _}) -> false;
|
||||
({"div07_bcom_nested.aes", _}) -> false;
|
||||
({"div08_bcom_simple.aes", _}) -> false;
|
||||
({_, _}) -> true
|
||||
end,
|
||||
ConcatTestFiles
|
||||
),
|
||||
%?debugFmt("ConcatTestFiles=~p", [ConcatTestFiles]),
|
||||
{"file = sum(tokens)",
|
||||
[concat_property(Name, Path) || {Name, Path} <- NonStupidFiles]}.
|
||||
|
||||
concat_property(FileName, FilePath) ->
|
||||
%?debugFmt("concat_property(~p, _)", [FileName]),
|
||||
FileChars = gsc:very_stable_file(FilePath),
|
||||
{FileName ++ ": file = sum(tokens)",
|
||||
fun() ->
|
||||
case gsc:tokens_from_file(FileChars) of
|
||||
{ok, SfcTokens} ->
|
||||
ConcatStr = concat_token_strs(SfcTokens, []),
|
||||
?assertEqual(FileChars, ConcatStr);
|
||||
_Error ->
|
||||
ok
|
||||
end
|
||||
end}.
|
||||
|
||||
concat_token_strs([#tk{str = S} | Rest], Acc) ->
|
||||
concat_token_strs(Rest, [Acc, S]);
|
||||
concat_token_strs([], Acc) ->
|
||||
unicode:characters_to_nfc_list(Acc).
|
||||
|
||||
% underscore marks this as a test *generator*
|
||||
div_test_() ->
|
||||
% divergence
|
||||
DivFiles = div_files(),
|
||||
%?debugFmt("DivFiles=~p", [DivFiles]),
|
||||
{"claude tokenizer divergences fixed",
|
||||
[tokens_match(N, P) || {N, P} <- DivFiles]}.
|
||||
|
||||
tokens_match(FileName, FilePath) ->
|
||||
%?debugFmt("tokens_match(~p, _)", [FileName]),
|
||||
% extracting data to be tested
|
||||
% i hate this so much but lazy and this is test code so who really cares.
|
||||
SoTokens = so_tokens_from_file(FilePath),
|
||||
SfTokens = gsc:gso_tokens_from_file(FilePath),
|
||||
{FileName ++ ": tokenizers_agree",
|
||||
fun() ->
|
||||
case {SoTokens, SfTokens} of
|
||||
{{ok, So}, {ok, Sf}} -> ?assertEqual(So, Sf);
|
||||
{{error, _}, {error, _}} -> ok;
|
||||
{{ok, _}, {error, _}} -> error("so_scan succeeded and gso_scan failed");
|
||||
{{error, _}, {ok, _}} -> error("so_scan failed and gso_scan succeded")
|
||||
end
|
||||
end}.
|
||||
|
||||
% that's right, we have to enter via converting the
|
||||
% bytes in the file to a list... lol
|
||||
so_tokens_from_file(F) ->
|
||||
{ok, Bytes} = file:read_file(F),
|
||||
S = binary_to_list(Bytes),
|
||||
so_scan:scan(S).
|
||||
@@ -0,0 +1,27 @@
|
||||
% testing utilities
|
||||
-module(ts_utils).
|
||||
|
||||
-export([
|
||||
ct_dir/0,
|
||||
ct_file/1, ct_file_abspath/1
|
||||
]).
|
||||
|
||||
|
||||
-spec ct_dir() -> string().
|
||||
|
||||
% directory containing the tests for the tokenizer
|
||||
ct_dir() ->
|
||||
zx_daemon:get_home() ++ "/ct".
|
||||
|
||||
|
||||
ct_file_abspath(Name) ->
|
||||
ct_file(Name).
|
||||
|
||||
-spec ct_file(Name) -> AbsPath when
|
||||
Name :: string(),
|
||||
AbsPath :: string().
|
||||
% @doc
|
||||
% ct_file("foo.aes") -> "/path/to/ct/foo.aes"
|
||||
|
||||
ct_file(Name) ->
|
||||
ct_dir() ++ "/" ++ Name.
|
||||
Reference in New Issue
Block a user